@salesforce/afv-skills 1.66.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 +2 -4
- package/skills/agentforce-offline-experimentation-run/SKILL.md +26 -49
- 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/commerce-react-storefront-create/SKILL.md +350 -0
- package/skills/consumer-goods-promotion-bo-api-deploy/SKILL.md +2 -2
- package/skills/consumer-goods-promotion-bo-api-deploy/references/generate-and-wire.md +28 -16
- package/skills/consumer-goods-promotion-bo-api-deploy/references/smoke-and-verify.md +18 -1
- 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 +22 -24
- package/skills/education-cloud-domain-configure/references/dependent-features.md +9 -0
- package/skills/education-cloud-domain-configure/references/domain-features.md +15 -0
- package/skills/education-cloud-domain-configure/references/gotchas-extended.md +8 -0
- package/skills/education-cloud-domain-configure/references/setup-widgets.md +123 -0
- 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 +19 -13
- package/skills/experience-search-coordinate/references/content-route.md +79 -1
- package/skills/experience-search-coordinate/references/media-route.md +30 -4
- package/skills/experience-ui-bundle-features-generate/SKILL.md +1 -1
- package/skills/experience-ui-bundle-mfa-configure/SKILL.md +1 -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-deploy/README.md +1 -1
- package/skills/platform-metadata-deploy/SKILL.md +114 -15
- package/skills/platform-metadata-deploy/references/agent-deployment-guide.md +3 -4
- package/skills/platform-metadata-deploy/references/orchestration.md +2 -3
- 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/platform-value-set-generate/SKILL.md +19 -19
- package/skills/platform-widget-generate/SKILL.md +25 -51
- package/skills/platform-widget-generate/examples/formulas.json +19 -2
- package/skills/platform-widget-generate/examples/localized-labels.json +57 -0
- package/skills/platform-widget-generate/references/widget-actions.md +29 -0
- package/skills/platform-widget-generate/references/widget-bundle-validation.md +1 -0
- package/skills/platform-widget-generate/references/widget-formulas.md +24 -7
- package/skills/platform-widget-generate/references/widget-gotchas.md +10 -0
- package/skills/platform-widget-generate/references/widget-labels.md +78 -0
- package/skills/platform-widget-generate/references/widget-styling.md +12 -0
- package/skills/sales-call-scoring-configure/SKILL.md +17 -10
- package/skills/sales-call-scoring-configure/scripts/enable-call-scoring.sh +55 -33
- package/skills/service-digital-engagement-channel-configure/SKILL.md +11 -7
- 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 +123 -16
- package/skills/service-digital-engagement-deployment-configure/SKILL.md +37 -100
- package/skills/service-digital-engagement-deployment-configure/assets/esd_web_update_template.xml +83 -8
- package/skills/service-digital-engagement-deployment-configure/examples/esd_web_prechat_fields.xml +74 -0
- package/skills/service-digital-engagement-deployment-configure/references/connect_api_creation.md +13 -3
- package/skills/service-digital-engagement-deployment-configure/references/deployment_settings.md +13 -78
- package/skills/service-digital-engagement-deployment-configure/references/onboarding_contract.md +5 -6
- package/skills/service-digital-engagement-deployment-configure/references/prechat_forms.md +245 -0
- package/skills/service-itsm-agentic-setup-incident-management/SKILL.md +164 -120
- package/skills/service-itsm-agentic-setup-incident-management/examples/output-templates.md +61 -37
- package/skills/service-itsm-agentic-setup-incident-management/references/audit-tracking.md +42 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/broadcast-communications.md +23 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/email-to-incident.md +23 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-dashboards.md +25 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-persona-psg.md +30 -146
- package/skills/service-itsm-agentic-setup-incident-management/references/incident-preferences.md +32 -136
- package/skills/service-itsm-agentic-setup-incident-management/references/major-incident-management.md +32 -155
- package/skills/service-itsm-agentic-setup-incident-management/references/manage-incidents-with-ai.md +45 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/priority-matrix.md +32 -0
- package/skills/service-itsm-agentic-setup-incident-management/references/service-mgmt-privilege.md +30 -140
- package/skills/service-itsm-agentic-setup-incident-management/references/sla-milestones.md +110 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""The same edit ops in the .agent file: the source-side twin of patch.py.
|
|
2
|
+
|
|
3
|
+
patch.py scores an edit by applying it to the captured requests. This makes that edit in the
|
|
4
|
+
agent's source, so the version that ships is the one that was scored:
|
|
5
|
+
|
|
6
|
+
new_text, report = agentpatch.patch_agent(text, patch.parse_ops(doc))
|
|
7
|
+
|
|
8
|
+
The targets are patch.py's, in the source:
|
|
9
|
+
node:<name> the node's reasoning instruction text blocks
|
|
10
|
+
system system.instructions, and the system.instructions override of a node in
|
|
11
|
+
scope (how an override renders is not verified on traces)
|
|
12
|
+
tool:<name>, transition:<name>
|
|
13
|
+
a transition binding's description; for an action binding, the
|
|
14
|
+
description of its action definition
|
|
15
|
+
tool:<name>.<param> that input's description, where the binding offers it (with p = ...)
|
|
16
|
+
|
|
17
|
+
find / anchor / with / text are matched and written verbatim, as .agent source text (dedented,
|
|
18
|
+
merge fields as written). Only the lines an edit changes are rewritten, at the block's indent
|
|
19
|
+
and in its | style; procedure statements are never touched. A quoted description stays one
|
|
20
|
+
quoted string, in JSON escapes (the compiler decodes \\" \\\\ \\n \\t); set_description adds a
|
|
21
|
+
missing description. The result is parsed back: it must be the same agent, with only the edited
|
|
22
|
+
texts changed, as the parser reads them.
|
|
23
|
+
|
|
24
|
+
An op is refused (AgentPatchError, its .reason one of REASONS) when the .agent edit could
|
|
25
|
+
differ from the request edit that was scored, or when it cannot be written:
|
|
26
|
+
no_target the agent lacks the target, or a node named in nodes
|
|
27
|
+
unmatched the find / anchor is in none of the target's texts
|
|
28
|
+
ambiguous it occurs more than once without all, counted over all of a node's blocks (each
|
|
29
|
+
branch of an if is a block); set_description over different descriptions
|
|
30
|
+
render the source edit renders otherwise than the same edit on the rendered text: the
|
|
31
|
+
find matches the rendered text but not the source ({!@actions.X} written as X),
|
|
32
|
+
or more often in the rendered text than in the source
|
|
33
|
+
shared the text is also used where the op does not apply: an action definition bound
|
|
34
|
+
to another tool, or in a node outside nodes; system.instructions with nodes that
|
|
35
|
+
leave out a node using it
|
|
36
|
+
unwritable the result cannot be written as intended, or does not read back as intended
|
|
37
|
+
(an empty block, a trailing blank line, a CR, a control character, ...)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import re
|
|
42
|
+
from difflib import SequenceMatcher
|
|
43
|
+
|
|
44
|
+
from .agentsrc import MERGE_FIELD, AgentSourceError, _child, _str, parse_agent, render_text
|
|
45
|
+
from .patch import edit_text
|
|
46
|
+
|
|
47
|
+
REASONS = ("no_target", "unmatched", "ambiguous", "render", "shared", "unwritable")
|
|
48
|
+
DESCRIPTION_INDENT = 4 # a description added to an entry without children: this much deeper
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AgentPatchError(ValueError):
|
|
52
|
+
def __init__(self, reason, message):
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.reason = reason
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _err(op, reason, msg):
|
|
58
|
+
return AgentPatchError(reason, f"op {op.id}: {msg}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class _Text:
|
|
62
|
+
"""One editable text: a block, a one-line value, or a missing description (text None)."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, where, block=None, item=None, owner=None):
|
|
65
|
+
self.where, self.block, self.item, self.owner = where, block, item, owner
|
|
66
|
+
if block is not None:
|
|
67
|
+
self.text = block.text
|
|
68
|
+
elif item is not None:
|
|
69
|
+
self.text = _str(item.value)
|
|
70
|
+
else:
|
|
71
|
+
self.text = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _description(where, owner):
|
|
75
|
+
it = _child(owner, "description")
|
|
76
|
+
if it is None:
|
|
77
|
+
return _Text(where, owner=owner)
|
|
78
|
+
if it.block is not None:
|
|
79
|
+
return _Text(where, block=it.block)
|
|
80
|
+
return _Text(where, item=it)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _editable(src):
|
|
84
|
+
"""{key: _Text} for every text an op can edit, and the descriptions of action bindings."""
|
|
85
|
+
out = {}
|
|
86
|
+
if src.system is not None:
|
|
87
|
+
out[("system", None)] = _Text("system.instructions", block=src.system)
|
|
88
|
+
for n, node in src.nodes.items():
|
|
89
|
+
if node.system is not None:
|
|
90
|
+
out[("system", n)] = _Text(f"the system.instructions of node {n}", block=node.system)
|
|
91
|
+
for k, b in enumerate(node.blocks):
|
|
92
|
+
out[("block", n, k)] = _Text(f"instruction block {k + 1} of node {n} (line {b.start})", block=b)
|
|
93
|
+
for b in node.bindings.values():
|
|
94
|
+
out[("binding", n, b.name)] = _description(f"the description of {b.name} in node {n}", b.item)
|
|
95
|
+
for d in node.action_defs.values():
|
|
96
|
+
out[("action", n, d.name)] = _description(f"the description of action {d.name} (node {n})", d.item)
|
|
97
|
+
for inp in d.inputs.values():
|
|
98
|
+
out[("input", n, d.name, inp.name)] = _description(
|
|
99
|
+
f"the description of input {inp.name} of action {d.name} (node {n})", inp.item)
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _shape(src):
|
|
104
|
+
"""All of the agent but its editable texts and line numbers: an edit must keep it."""
|
|
105
|
+
nodes = []
|
|
106
|
+
for node in src.nodes.values():
|
|
107
|
+
nodes.append((node.kind, node.name, node.label, node.description, node.model, node.procedure,
|
|
108
|
+
node.system is not None, [(b.style, b.conditional) for b in node.blocks],
|
|
109
|
+
[(b.name, b.kind, b.target, b.llm_inputs, sorted(b.bound_inputs.items()), b.available_when)
|
|
110
|
+
for b in node.bindings.values()],
|
|
111
|
+
[(d.name, d.label, d.target,
|
|
112
|
+
[(i.name, i.type, i.label, i.is_required, i.is_user_input) for i in d.inputs.values()])
|
|
113
|
+
for d in node.action_defs.values()]))
|
|
114
|
+
return src.model, src.developer_name, src.system is not None, nodes
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------------------
|
|
118
|
+
# targets
|
|
119
|
+
|
|
120
|
+
def _home(src, d):
|
|
121
|
+
for n, node in src.nodes.items():
|
|
122
|
+
if node.action_defs.get(d.name) is d:
|
|
123
|
+
return n
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _scope(src, op):
|
|
128
|
+
if op.nodes is None:
|
|
129
|
+
return list(src.nodes)
|
|
130
|
+
missing = sorted(op.nodes - set(src.nodes))
|
|
131
|
+
if missing:
|
|
132
|
+
raise _err(op, "no_target", f"nodes {missing} are not in the agent")
|
|
133
|
+
return [n for n in src.nodes if n in op.nodes]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _targets(src, texts, op):
|
|
137
|
+
"""[(key, _Text)] of the texts the op applies to."""
|
|
138
|
+
if op.kind == "node":
|
|
139
|
+
node = src.nodes.get(op.name)
|
|
140
|
+
if node is None:
|
|
141
|
+
raise _err(op, "no_target", f"the agent has no node {op.name}")
|
|
142
|
+
if not node.blocks:
|
|
143
|
+
raise _err(op, "no_target", f"node {op.name} has no instruction text")
|
|
144
|
+
keys = [("block", op.name, k) for k in range(len(node.blocks))]
|
|
145
|
+
elif op.kind == "system":
|
|
146
|
+
keys = _system_keys(src, op, _scope(src, op))
|
|
147
|
+
else:
|
|
148
|
+
keys = _description_keys(src, op, _scope(src, op))
|
|
149
|
+
return [(k, texts[k]) for k in keys]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _system_keys(src, op, scope):
|
|
153
|
+
users = [n for n, node in src.nodes.items() if node.system is None] # nodes on the global text
|
|
154
|
+
keys = []
|
|
155
|
+
if src.system is not None and (op.nodes is None or any(n in op.nodes for n in users)):
|
|
156
|
+
left = [n for n in users if op.nodes is not None and n not in op.nodes]
|
|
157
|
+
if left:
|
|
158
|
+
raise _err(op, "shared", f"system.instructions is also the system text of nodes {left}, which nodes "
|
|
159
|
+
"leaves out: list them too, or drop nodes")
|
|
160
|
+
keys.append(("system", None))
|
|
161
|
+
keys += [("system", n) for n in scope if src.nodes[n].system is not None]
|
|
162
|
+
if not keys:
|
|
163
|
+
raise _err(op, "no_target", "no system.instructions in "
|
|
164
|
+
+ ("the agent" if op.nodes is None else f"nodes {sorted(op.nodes)}"))
|
|
165
|
+
return keys
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _description_keys(src, op, scope):
|
|
169
|
+
keys, defs = [], []
|
|
170
|
+
for n in scope:
|
|
171
|
+
node = src.nodes[n]
|
|
172
|
+
b = node.bindings.get(op.name)
|
|
173
|
+
if b is None:
|
|
174
|
+
continue
|
|
175
|
+
if b.kind == "transition":
|
|
176
|
+
if op.param:
|
|
177
|
+
raise _err(op, "no_target", f"{op.name} in node {n} is a transition: it has no parameters")
|
|
178
|
+
key = ("binding", n, op.name)
|
|
179
|
+
elif b.kind == "action":
|
|
180
|
+
d = src.action_def(node, b.action)
|
|
181
|
+
if d is None:
|
|
182
|
+
raise _err(op, "no_target", f"{op.name} in node {n} is bound to @actions.{b.action}, "
|
|
183
|
+
"which the agent does not define")
|
|
184
|
+
if op.param:
|
|
185
|
+
if op.param not in d.inputs:
|
|
186
|
+
raise _err(op, "no_target", f"action {d.name} has no input {op.param}")
|
|
187
|
+
if op.param not in b.llm_inputs:
|
|
188
|
+
continue # bound to an expression: not a parameter of the tool
|
|
189
|
+
key = ("input", _home(src, d), d.name, op.param)
|
|
190
|
+
else:
|
|
191
|
+
key = ("action", _home(src, d), d.name)
|
|
192
|
+
if d not in defs:
|
|
193
|
+
defs.append(d)
|
|
194
|
+
else:
|
|
195
|
+
raise _err(op, "no_target", f"{op.name} in node {n} is neither an action nor a transition")
|
|
196
|
+
if key not in keys:
|
|
197
|
+
keys.append(key)
|
|
198
|
+
if not keys:
|
|
199
|
+
where = "the agent" if op.nodes is None else f"nodes {sorted(op.nodes)}"
|
|
200
|
+
offers = f" with {op.param} = ..." if op.param else ""
|
|
201
|
+
raise _err(op, "no_target", f"no node of {where} binds {op.name}{offers}")
|
|
202
|
+
for d in defs:
|
|
203
|
+
_check_shared_def(src, op, d)
|
|
204
|
+
return keys
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _check_shared_def(src, op, d):
|
|
208
|
+
"""An action definition's text is the description of every tool bound to it: each of
|
|
209
|
+
those must be the op's tool, in its scope, or the .agent edit changes more tools than the
|
|
210
|
+
request edit did."""
|
|
211
|
+
others = []
|
|
212
|
+
for n, node in src.nodes.items():
|
|
213
|
+
for b in node.bindings.values():
|
|
214
|
+
if b.kind != "action" or src.action_def(node, b.action) is not d:
|
|
215
|
+
continue
|
|
216
|
+
if op.param and op.param not in b.llm_inputs:
|
|
217
|
+
continue
|
|
218
|
+
if b.name != op.name or (op.nodes is not None and n not in op.nodes):
|
|
219
|
+
others.append(f"{b.name} in node {n}")
|
|
220
|
+
if others:
|
|
221
|
+
what = f"input {op.param} of action {d.name}" if op.param else f"action {d.name}"
|
|
222
|
+
raise _err(op, "shared", f"the description of {what} is also that of {', '.join(others)}, which the op "
|
|
223
|
+
"leaves alone: widen nodes, or give the tool its own action definition first")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------------------
|
|
227
|
+
# the edits: {key: new text}
|
|
228
|
+
|
|
229
|
+
def _what(op):
|
|
230
|
+
word = "find" if op.op in ("replace", "delete") else "anchor"
|
|
231
|
+
loc = op.locator if len(op.locator) <= 60 else op.locator[:57] + "..."
|
|
232
|
+
return f"the {word} {loc!r}"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _where(targets):
|
|
236
|
+
if len(targets) == 1:
|
|
237
|
+
return targets[0][1].where
|
|
238
|
+
return f"{len(targets)} texts ({'; '.join(t.where for _, t in targets)})"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _verbatim(text, op, pattern):
|
|
242
|
+
out, pos = [], 0
|
|
243
|
+
for m in pattern.finditer(text):
|
|
244
|
+
if op.op == "insert_after":
|
|
245
|
+
out += [text[pos:m.end()], op.new]
|
|
246
|
+
elif op.op == "insert_before":
|
|
247
|
+
out += [text[pos:m.start()], op.new, m.group(0)]
|
|
248
|
+
else:
|
|
249
|
+
out += [text[pos:m.start()], op.new]
|
|
250
|
+
pos = m.end()
|
|
251
|
+
out.append(text[pos:])
|
|
252
|
+
return "".join(out)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _cut_field(text, pattern):
|
|
256
|
+
"""The first merge field that a match of pattern starts or ends inside of, if any."""
|
|
257
|
+
fields = [(m.start(), m.end(), m.group(0)) for m in MERGE_FIELD.finditer(text)]
|
|
258
|
+
for m in pattern.finditer(text):
|
|
259
|
+
for start, end, field in fields:
|
|
260
|
+
if start < m.end() and m.start() < end and not (m.start() <= start and end <= m.end()):
|
|
261
|
+
return field
|
|
262
|
+
return None
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _block_edits(op, targets):
|
|
266
|
+
"""The verbatim source edit of each block, checked against the request edit of its
|
|
267
|
+
rendered text (patch.py's, which the replay scored)."""
|
|
268
|
+
pattern = re.compile(re.escape(op.locator))
|
|
269
|
+
for _, t in targets:
|
|
270
|
+
field = _cut_field(t.text, pattern)
|
|
271
|
+
if field:
|
|
272
|
+
raise _err(op, "render", f"{_what(op)} cuts into the merge field {field} in {t.where}: the edit "
|
|
273
|
+
"would change what it refers to. Find whole merge fields, as written")
|
|
274
|
+
found = {key: len(pattern.findall(t.text)) for key, t in targets}
|
|
275
|
+
for key, t in targets:
|
|
276
|
+
if not found[key] and edit_text(render_text(t.text), op)[1] != "unmatched":
|
|
277
|
+
raise _err(op, "render", f"{_what(op)} matches the rendered text of {t.where} but not its source: copy "
|
|
278
|
+
"it from the .agent, with merge fields ({!@actions.X}) and the indentation of "
|
|
279
|
+
"its lines as written")
|
|
280
|
+
total = sum(found.values())
|
|
281
|
+
if not total:
|
|
282
|
+
raise _err(op, "unmatched", f"{_what(op)} is not in {_where(targets)}: copy it from the .agent")
|
|
283
|
+
hit = [key for key, _ in targets if found[key]]
|
|
284
|
+
if total > 1 and not op.all:
|
|
285
|
+
branches = " (in different blocks: each branch of an if is one)" if len(hit) > 1 else ""
|
|
286
|
+
raise _err(op, "ambiguous", f"{_what(op)} occurs {total} times in {_where(targets)}{branches}: "
|
|
287
|
+
"lengthen it, or set all")
|
|
288
|
+
edits = {}
|
|
289
|
+
for key, t in targets:
|
|
290
|
+
if not found[key]:
|
|
291
|
+
continue
|
|
292
|
+
new = _verbatim(t.text, op, pattern)
|
|
293
|
+
req_new, outcome = edit_text(render_text(t.text), op)
|
|
294
|
+
if outcome == "ambiguous":
|
|
295
|
+
raise _err(op, "render", f"{_what(op)} occurs once in the source of {t.where} but more often in its "
|
|
296
|
+
"rendered text ({!@actions.X} renders as X): lengthen it")
|
|
297
|
+
if outcome != "matched" or render_text(new) != req_new:
|
|
298
|
+
raise _err(op, "render", f"the edit of {t.where} renders otherwise than the same edit on its rendered "
|
|
299
|
+
"text (a line that starts with a merge field loses its indent): keep merge "
|
|
300
|
+
"fields off the start of a line the edit changes")
|
|
301
|
+
edits[key] = new
|
|
302
|
+
return edits
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _description_edits(op, targets):
|
|
306
|
+
if op.op == "set_description":
|
|
307
|
+
current = {t.text for _, t in targets}
|
|
308
|
+
if len(current) > 1:
|
|
309
|
+
raise _err(op, "ambiguous", f"{op.target} has {len(current)} different descriptions in scope "
|
|
310
|
+
f"({'; '.join(t.where for _, t in targets)}): limit the op with nodes")
|
|
311
|
+
return {key: op.new for key, _ in targets}
|
|
312
|
+
edits, matched = {}, False
|
|
313
|
+
for key, t in targets:
|
|
314
|
+
new, outcome = edit_text(t.text or "", op)
|
|
315
|
+
if outcome == "ambiguous":
|
|
316
|
+
raise _err(op, "ambiguous", f"{_what(op)} occurs more than once in {t.where}: lengthen it, or set all")
|
|
317
|
+
if outcome == "matched":
|
|
318
|
+
matched = True
|
|
319
|
+
edits[key] = new
|
|
320
|
+
if not matched:
|
|
321
|
+
raise _err(op, "unmatched", f"{_what(op)} is not in {_where(targets)}: copy it from the .agent")
|
|
322
|
+
return edits
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# ---------------------------------------------------------------------------------------
|
|
326
|
+
# writing: (first line index, end index, new raw lines) splices of the file's lines
|
|
327
|
+
|
|
328
|
+
def _block_raws(op, t, new):
|
|
329
|
+
"""The block's new raw lines: unchanged lines as they were, changed ones at its indent."""
|
|
330
|
+
b = t.block
|
|
331
|
+
if not b.lines:
|
|
332
|
+
raise _err(op, "unwritable", f"{t.where} has no text lines")
|
|
333
|
+
old, lines = b.text.split("\n"), new.split("\n")
|
|
334
|
+
if not new.strip():
|
|
335
|
+
raise _err(op, "unwritable", f"the edit would leave {t.where} empty")
|
|
336
|
+
if not lines[-1].strip():
|
|
337
|
+
raise _err(op, "unwritable", f"the edit would end {t.where} with a blank line, which is not part of a block "
|
|
338
|
+
"in the .agent: take the line break before the text into the find instead")
|
|
339
|
+
if any(ln and not ln.strip() for ln in lines):
|
|
340
|
+
raise _err(op, "unwritable", f"the edit would give {t.where} a line of only spaces")
|
|
341
|
+
inline = b.style == "inline"
|
|
342
|
+
if inline and (not lines[0] or lines[0].startswith(" ")):
|
|
343
|
+
raise _err(op, "unwritable", f"the edit would leave the | line of {t.where} empty or starting with a space")
|
|
344
|
+
if not inline and next(ln for ln in lines if ln).startswith(" "):
|
|
345
|
+
raise _err(op, "unwritable", f"the edit would indent the first text line of {t.where}, which sets the "
|
|
346
|
+
"block's indent")
|
|
347
|
+
raws = [raw for _, raw in b.lines]
|
|
348
|
+
|
|
349
|
+
def fmt(j, text):
|
|
350
|
+
if inline and j == 0:
|
|
351
|
+
return raws[0][:b.base] + text
|
|
352
|
+
return " " * b.base + text if text else ""
|
|
353
|
+
|
|
354
|
+
out = []
|
|
355
|
+
for tag, i1, _, j1, j2 in SequenceMatcher(None, old, lines, autojunk=False).get_opcodes():
|
|
356
|
+
for d in range(j2 - j1):
|
|
357
|
+
i, j = i1 + d, j1 + d
|
|
358
|
+
keep = tag == "equal" and not (inline and (i == 0) != (j == 0))
|
|
359
|
+
out.append(raws[i] if keep else fmt(j, lines[j]))
|
|
360
|
+
return out
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def _string(op, t, new):
|
|
364
|
+
bad = sorted({hex(ord(c)) for c in new if ord(c) < 32 and c not in "\n\t"})
|
|
365
|
+
if bad:
|
|
366
|
+
raise _err(op, "unwritable", f"the new text of {t.where} holds control characters {bad}")
|
|
367
|
+
return json.dumps(new, ensure_ascii=False)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _quoted_raw(op, t, new, lines):
|
|
371
|
+
it = t.item
|
|
372
|
+
body = lines[it.line - 1].rstrip()
|
|
373
|
+
try:
|
|
374
|
+
one_string = isinstance(json.loads(it.value), str)
|
|
375
|
+
except ValueError:
|
|
376
|
+
one_string = False
|
|
377
|
+
if it.children or not one_string or not body.endswith(it.value):
|
|
378
|
+
raise _err(op, "unwritable", f"{t.where} is not a one-line quoted string: make it one, or a "
|
|
379
|
+
"description: | block, first")
|
|
380
|
+
return body[:len(body) - len(it.value)] + _string(op, t, new)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _missing_raw(op, t, new):
|
|
384
|
+
owner = t.owner
|
|
385
|
+
indent = owner.children[0].indent if owner.children else owner.indent + DESCRIPTION_INDENT
|
|
386
|
+
return " " * indent + "description: " + _string(op, t, new)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
_KINDS = {"block": "instructions", "system": "system", "binding": "transition", "action": "action",
|
|
390
|
+
"input": "input"}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _about(key):
|
|
394
|
+
out = {"text": _KINDS[key[0]]}
|
|
395
|
+
if key[0] == "block":
|
|
396
|
+
out.update(node=key[1], block=key[2])
|
|
397
|
+
elif key[0] == "system":
|
|
398
|
+
if key[1] is not None:
|
|
399
|
+
out["node"] = key[1]
|
|
400
|
+
else:
|
|
401
|
+
out.update(node=key[1], name=key[2])
|
|
402
|
+
if key[0] == "input":
|
|
403
|
+
out["input"] = key[3]
|
|
404
|
+
return out
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _hunks(key, start, old, new):
|
|
408
|
+
out = []
|
|
409
|
+
for tag, i1, i2, j1, j2 in SequenceMatcher(None, old, new, autojunk=False).get_opcodes():
|
|
410
|
+
if tag != "equal":
|
|
411
|
+
out.append(dict(_about(key), line=start + i1 + 1, removed=old[i1:i2], added=new[j1:j2]))
|
|
412
|
+
return out
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _apply(text, src, op):
|
|
416
|
+
"""(new text, new source, report edits) of one op."""
|
|
417
|
+
if "\r" in op.new:
|
|
418
|
+
raise _err(op, "unwritable", "the op's text holds a carriage return")
|
|
419
|
+
texts = _editable(src)
|
|
420
|
+
targets = _targets(src, texts, op)
|
|
421
|
+
edits = (_description_edits if op.is_description else _block_edits)(op, targets)
|
|
422
|
+
lines = text.split("\n")
|
|
423
|
+
splices = []
|
|
424
|
+
for key, t in targets:
|
|
425
|
+
new = edits.get(key)
|
|
426
|
+
if new is None or new == t.text:
|
|
427
|
+
continue
|
|
428
|
+
if t.block is not None:
|
|
429
|
+
splices.append((t.block.start - 1, t.block.end, _block_raws(op, t, new), key))
|
|
430
|
+
elif t.item is not None:
|
|
431
|
+
splices.append((t.item.line - 1, t.item.line, [_quoted_raw(op, t, new, lines)], key))
|
|
432
|
+
else:
|
|
433
|
+
splices.append((t.owner.line, t.owner.line, [_missing_raw(op, t, new)], key))
|
|
434
|
+
report = []
|
|
435
|
+
for i0, i1, raws, key in sorted(splices, key=lambda s: s[0], reverse=True):
|
|
436
|
+
report[:0] = _hunks(key, i0, lines[i0:i1], raws)
|
|
437
|
+
lines[i0:i1] = raws
|
|
438
|
+
new_text = "\n".join(lines)
|
|
439
|
+
return new_text, _read_back(op, src, texts, new_text, edits), report
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _parsed(src, key):
|
|
443
|
+
"""The text at key as the parser reads it, which is what node_tools gives the scorer; it is
|
|
444
|
+
the one _editable found unless an entry holds two descriptions (the parser keeps the last)."""
|
|
445
|
+
if key[0] == "system":
|
|
446
|
+
return (src.system if key[1] is None else src.nodes[key[1]].system).text
|
|
447
|
+
node = src.nodes[key[1]]
|
|
448
|
+
if key[0] == "block":
|
|
449
|
+
return node.blocks[key[2]].text
|
|
450
|
+
if key[0] == "binding":
|
|
451
|
+
return node.bindings[key[2]].description
|
|
452
|
+
d = node.action_defs[key[2]]
|
|
453
|
+
return d.description if key[0] == "action" else d.inputs[key[3]].description
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _read_back(op, src, texts, new_text, edits):
|
|
457
|
+
"""The edited agent, parsed: the same agent with only the edited texts changed."""
|
|
458
|
+
try:
|
|
459
|
+
new_src = parse_agent(new_text, src.path)
|
|
460
|
+
except AgentSourceError as e:
|
|
461
|
+
raise _err(op, "unwritable", f"the edited agent does not parse: {e}")
|
|
462
|
+
if _shape(new_src) != _shape(src):
|
|
463
|
+
raise _err(op, "unwritable", "the edit changes more than texts (nodes, blocks, bindings or definitions "
|
|
464
|
+
"read back otherwise)")
|
|
465
|
+
wrong = [t.where for key, t in texts.items() if _parsed(new_src, key) != edits.get(key, _parsed(src, key))]
|
|
466
|
+
if wrong:
|
|
467
|
+
raise _err(op, "unwritable", f"the edited agent reads back otherwise in {'; '.join(wrong)}")
|
|
468
|
+
return new_src
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def patch_agent(text, ops, path=None):
|
|
472
|
+
"""(new text, report) of the ops applied in order to an .agent's source text, each to the
|
|
473
|
+
result of the ones before; no ops gives the text unchanged. The report has one entry per
|
|
474
|
+
op, {id, target, op, edits}; an edit is {text, node, block | name [, input], line,
|
|
475
|
+
removed, added}, its line 1-based in the text the op applied to."""
|
|
476
|
+
if "\r" in text:
|
|
477
|
+
raise AgentPatchError("unwritable", f"{path or 'agent'}: has CR line endings; convert it to LF first")
|
|
478
|
+
src = parse_agent(text, path)
|
|
479
|
+
report = []
|
|
480
|
+
for op in ops:
|
|
481
|
+
text, src, edits = _apply(text, src, op)
|
|
482
|
+
report.append({"id": op.id, "target": op.target, "op": op.op, "edits": edits})
|
|
483
|
+
return text, report
|