@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
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""One replay unit per production LLM_STEP (schema bt.unit/1).
|
|
2
|
+
|
|
3
|
+
A turn makes several LLM calls: the router picks a sub-agent (go_to_*), then the sub-agent
|
|
4
|
+
calls tools and finally replies. Each call is its own unit, with its own request and its own
|
|
5
|
+
golden (that step's gen_ai.output.messages[0]) — never the turn's final reply and never the
|
|
6
|
+
ACTION_STEP's platform action name (Shopper_Add_To_Cart is not the AddToCart tool the model
|
|
7
|
+
called).
|
|
8
|
+
|
|
9
|
+
unit_id = "<session_id>:<turn index>:<step ordinal>"; the ordinal counts every LLM_STEP of
|
|
10
|
+
the turn ordered by (start_time, step_id), skipped ones included, so ids do not depend on
|
|
11
|
+
filters.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
from collections import Counter
|
|
17
|
+
|
|
18
|
+
from . import SCHEMA_UNIT
|
|
19
|
+
from .messages import build_messages
|
|
20
|
+
|
|
21
|
+
DEFAULT_ROUTER_NODES = ("agent_router", "topic_selector")
|
|
22
|
+
GROUNDEDNESS_NODE = "Atlas__GroundednessValidationPrompt"
|
|
23
|
+
# LLM steps that validate a reply instead of making an agent decision. They carry no
|
|
24
|
+
# input messages either; listed so the skip reason is explicit.
|
|
25
|
+
NON_DECISION_NODES = (GROUNDEDNESS_NODE,)
|
|
26
|
+
# What production's own checks said of a turn. The steps are not replayed, and their results are
|
|
27
|
+
# text: "category=GROUNDED, reason=..." and "InstructionAdherence: value=UNCERTAIN, ...;
|
|
28
|
+
# TaskResolution: value=...". Only these values are kept, never the reasons.
|
|
29
|
+
_GROUNDEDNESS = re.compile(r"\bcategory=([A-Z][A-Z_]*)")
|
|
30
|
+
_GUARDRAIL = re.compile(r"\b([A-Za-z]\w*): value=([A-Z][A-Z_]*)")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load(text):
|
|
34
|
+
if not text:
|
|
35
|
+
return None
|
|
36
|
+
try:
|
|
37
|
+
return json.loads(text)
|
|
38
|
+
except (TypeError, json.JSONDecodeError):
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_finish_reasons(value):
|
|
43
|
+
""""('tool_calls',)" -> 'tool_calls'; lists and plain strings pass through."""
|
|
44
|
+
if isinstance(value, (list, tuple)):
|
|
45
|
+
return value[0] if value else None
|
|
46
|
+
if not isinstance(value, str):
|
|
47
|
+
return None
|
|
48
|
+
v = value.strip().strip("()[]").strip().rstrip(",").strip().strip("'\"")
|
|
49
|
+
return v or None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def parse_arguments(arguments):
|
|
53
|
+
"""(args dict, raw string or None). Unparseable arguments are kept raw."""
|
|
54
|
+
if isinstance(arguments, dict):
|
|
55
|
+
return arguments, None
|
|
56
|
+
if arguments in (None, ""):
|
|
57
|
+
return {}, None
|
|
58
|
+
if isinstance(arguments, str):
|
|
59
|
+
try:
|
|
60
|
+
v = json.loads(arguments)
|
|
61
|
+
if isinstance(v, dict):
|
|
62
|
+
return v, None
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
pass
|
|
65
|
+
return {}, arguments if isinstance(arguments, str) else json.dumps(arguments)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def golden_from_output(output):
|
|
69
|
+
"""The step's own answer, from gen_ai.output.messages[0]: every tool call, or the text."""
|
|
70
|
+
msgs = (output or {}).get("gen_ai.output.messages") or []
|
|
71
|
+
if not msgs:
|
|
72
|
+
return None
|
|
73
|
+
m = msgs[0]
|
|
74
|
+
finish = parse_finish_reasons(output.get("gen_ai.response.finish_reasons")) or m.get("finish_reason")
|
|
75
|
+
calls = []
|
|
76
|
+
for inv in m.get("tool_invocations") or []:
|
|
77
|
+
fn = inv.get("function") or {}
|
|
78
|
+
args, raw = parse_arguments(fn.get("arguments"))
|
|
79
|
+
call = {"name": fn.get("name"), "args": args}
|
|
80
|
+
if raw is not None:
|
|
81
|
+
call["args_raw"] = raw
|
|
82
|
+
calls.append(call)
|
|
83
|
+
golden = {"kind": "tool" if calls else "text", "tool_calls": calls,
|
|
84
|
+
"text": m.get("content") or "", "finish_reason": finish}
|
|
85
|
+
if len(msgs) > 1:
|
|
86
|
+
golden["extra_output_messages"] = len(msgs) - 1
|
|
87
|
+
return golden
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _step_result(step):
|
|
91
|
+
out = _load(step.get("output"))
|
|
92
|
+
result = out.get("mgr.sensitive.step.result") if isinstance(out, dict) else None
|
|
93
|
+
return result if isinstance(result, str) else ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def production_signals(convo, turn):
|
|
97
|
+
"""Report-only: production's own checks of a turn, which never change a score (the golden is
|
|
98
|
+
production's answer, right or wrong). groundedness: the category the groundedness check gave
|
|
99
|
+
the turn's reply (GROUNDED, PARTIALLY_GROUNDED_SUGGESTION, SMALL_TALK, ...); guardrails: the
|
|
100
|
+
value of each trust guardrail (InstructionAdherence, TaskResolution); step_errors: the turn's
|
|
101
|
+
steps with an error; and the session's end type and action error count. None or {} where STDM
|
|
102
|
+
has no such record."""
|
|
103
|
+
steps = sorted(turn.get("steps") or [], key=lambda s: (s.get("start_time") or "", s.get("step_id") or ""))
|
|
104
|
+
groundedness, guardrails = None, {}
|
|
105
|
+
for s in steps:
|
|
106
|
+
if s.get("step_type") == "LLM_STEP" and s.get("name") == GROUNDEDNESS_NODE:
|
|
107
|
+
m = _GROUNDEDNESS.search(_step_result(s))
|
|
108
|
+
groundedness = m.group(1) if m else groundedness
|
|
109
|
+
elif s.get("step_type") == "TRUST_GUARDRAILS_STEP":
|
|
110
|
+
guardrails.update(_GUARDRAIL.findall(_step_result(s)))
|
|
111
|
+
return {"groundedness": groundedness, "guardrails": guardrails,
|
|
112
|
+
"step_errors": sum(1 for s in steps if s.get("error")),
|
|
113
|
+
"session_end_type": convo.get("end_type"), "session_action_errors": convo.get("action_error_count")}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _turn_user_text(turn):
|
|
117
|
+
for msg in turn.get("messages") or []:
|
|
118
|
+
if msg.get("message_type") == "Input" and msg.get("text"):
|
|
119
|
+
return msg["text"].strip()
|
|
120
|
+
return ""
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_units(conversations, org=None, agent=None, router_nodes=DEFAULT_ROUTER_NODES,
|
|
124
|
+
steps="all"):
|
|
125
|
+
"""(units, skipped Counter). steps: 'router' | 'subagent' | 'all'."""
|
|
126
|
+
if steps not in ("router", "subagent", "all"):
|
|
127
|
+
raise ValueError(f"steps must be router|subagent|all, not {steps!r}")
|
|
128
|
+
router_nodes = set(router_nodes)
|
|
129
|
+
units, skipped = [], Counter()
|
|
130
|
+
for convo in conversations:
|
|
131
|
+
sid = convo.get("session_id")
|
|
132
|
+
for t_idx, turn in enumerate(convo.get("turns") or []):
|
|
133
|
+
llm = [s for s in turn.get("steps") or [] if s.get("step_type") == "LLM_STEP"]
|
|
134
|
+
llm.sort(key=lambda s: (s.get("start_time") or "", s.get("step_id") or ""))
|
|
135
|
+
for ordinal, step in enumerate(llm):
|
|
136
|
+
node = step.get("name") or ""
|
|
137
|
+
if node in NON_DECISION_NODES:
|
|
138
|
+
skipped["non_decision_step"] += 1
|
|
139
|
+
continue
|
|
140
|
+
inp = _load(step.get("input"))
|
|
141
|
+
if inp is None:
|
|
142
|
+
skipped["input_unparseable" if step.get("input") else "no_input"] += 1
|
|
143
|
+
continue
|
|
144
|
+
stdm_msgs = inp.get("gen_ai.input.messages")
|
|
145
|
+
if not stdm_msgs:
|
|
146
|
+
skipped["no_input_messages"] += 1
|
|
147
|
+
continue
|
|
148
|
+
out = _load(step.get("output"))
|
|
149
|
+
golden = golden_from_output(out)
|
|
150
|
+
if golden is None:
|
|
151
|
+
skipped["no_output_message"] += 1
|
|
152
|
+
continue
|
|
153
|
+
kind = "router" if node in router_nodes else "subagent"
|
|
154
|
+
if steps != "all" and steps != kind:
|
|
155
|
+
skipped["not_selected"] += 1
|
|
156
|
+
continue
|
|
157
|
+
units.append({
|
|
158
|
+
"schema": SCHEMA_UNIT,
|
|
159
|
+
"unit_id": f"{sid}:{t_idx}:{ordinal}",
|
|
160
|
+
"session_id": sid,
|
|
161
|
+
"turn": t_idx,
|
|
162
|
+
"step": ordinal,
|
|
163
|
+
"step_id": step.get("step_id"),
|
|
164
|
+
"interaction_id": turn.get("interaction_id"),
|
|
165
|
+
"org": org,
|
|
166
|
+
"agent": agent,
|
|
167
|
+
"agent_version": None,
|
|
168
|
+
"node": node,
|
|
169
|
+
"step_kind": kind,
|
|
170
|
+
"request": {
|
|
171
|
+
"messages": build_messages(stdm_msgs),
|
|
172
|
+
"tools": inp.get("gen_ai.request.tools") or [],
|
|
173
|
+
"model": inp.get("gen_ai.request.model"),
|
|
174
|
+
"settings": {"prompt_template": inp.get("af.prompt_template_dev_name"),
|
|
175
|
+
"stream": inp.get("gen_ai.request.stream")},
|
|
176
|
+
},
|
|
177
|
+
"golden": golden,
|
|
178
|
+
"split": None,
|
|
179
|
+
"channel": convo.get("channel"),
|
|
180
|
+
"topic": turn.get("topic"),
|
|
181
|
+
"user_utterance": _turn_user_text(turn),
|
|
182
|
+
"start_time": step.get("start_time"),
|
|
183
|
+
"production": production_signals(convo, turn),
|
|
184
|
+
})
|
|
185
|
+
return units, skipped
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def tool_names(unit):
|
|
189
|
+
return [(t.get("function") or {}).get("name") for t in unit["request"]["tools"]]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def tool_schemas(unit):
|
|
193
|
+
"""{tool name: parameters schema} for the tools offered to this step."""
|
|
194
|
+
out = {}
|
|
195
|
+
for t in unit["request"]["tools"]:
|
|
196
|
+
fn = t.get("function") or {}
|
|
197
|
+
if fn.get("name"):
|
|
198
|
+
out[fn["name"]] = fn.get("parameters") or {}
|
|
199
|
+
return out
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Which agent version each unit was rendered from (--agent-versions).
|
|
2
|
+
|
|
3
|
+
STDM records no agent version per step, and one pull can span several versions. A version
|
|
4
|
+
renders a unit when, for the unit's node:
|
|
5
|
+
instruction its text blocks render to the unit's instruction text (the last system
|
|
6
|
+
message, before "\\n\\n# CONTEXT"): the blocks in order, a block inside an
|
|
7
|
+
if / else possibly absent, merge fields matching any value
|
|
8
|
+
tools the tools offered are the node's, with this version's descriptions,
|
|
9
|
+
parameters and required lists (platform tools such as
|
|
10
|
+
__end_session_action__ aside; a tool with `available when` may be absent)
|
|
11
|
+
system its system text ends the first system message, or sits in it as whole lines
|
|
12
|
+
with the platform's own text after it (a voice agent); there, not where
|
|
13
|
+
another version's longer system text holds the same lines, as when a version
|
|
14
|
+
only added lines to it
|
|
15
|
+
Most steps render from several versions: a node that did not change between them renders
|
|
16
|
+
the same. A session runs one version, so the session's version is the one that renders
|
|
17
|
+
every unit of it that renders at all: the intersection. A unit's agent_version is its
|
|
18
|
+
session's version when that is a single one; compatible_versions lists the versions that
|
|
19
|
+
render the unit itself.
|
|
20
|
+
|
|
21
|
+
How several blocks of a node are joined is not verified (agentsrc.py), so any whitespace
|
|
22
|
+
may separate them. A loose check only adds versions to a unit's set, which can make a
|
|
23
|
+
session ambiguous but not wrong: the version that served a step renders it, so it is always
|
|
24
|
+
in the intersection.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import hashlib
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
from collections import Counter, defaultdict
|
|
31
|
+
|
|
32
|
+
from . import SCHEMA_VERSIONS
|
|
33
|
+
from .agentsrc import CONTEXT_MARKER, fragment_pattern
|
|
34
|
+
|
|
35
|
+
PLATFORM_TOOL = re.compile(r"^__\w+__$")
|
|
36
|
+
CHECKS = ("node", "instruction", "tools", "system") # in the order they are tried
|
|
37
|
+
_WHOLE_LINES = r"(?m)^[ \t]*(?:%s)[ \t]*$"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def unit_instruction(unit):
|
|
41
|
+
"""The node's instruction text in a unit's request ('' without a trailing system message)."""
|
|
42
|
+
messages = unit["request"]["messages"]
|
|
43
|
+
if len(messages) < 2 or messages[-1].get("role") != "system":
|
|
44
|
+
return ""
|
|
45
|
+
return (messages[-1].get("content") or "").split(CONTEXT_MARKER)[0]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _tools_agree(unit, tools):
|
|
49
|
+
offered = {}
|
|
50
|
+
for t in unit["request"].get("tools") or []:
|
|
51
|
+
fn = t.get("function") or {}
|
|
52
|
+
if fn.get("name") and not PLATFORM_TOOL.match(fn["name"]):
|
|
53
|
+
offered[fn["name"]] = fn
|
|
54
|
+
if set(offered) - set(tools) or {n for n, t in tools.items() if not t["conditional"]} - set(offered):
|
|
55
|
+
return False
|
|
56
|
+
for name, fn in offered.items():
|
|
57
|
+
mine = tools[name]
|
|
58
|
+
if mine["description"] is None: # not defined in the .agent: only its presence is known
|
|
59
|
+
continue
|
|
60
|
+
params = fn.get("parameters") or {}
|
|
61
|
+
props = params.get("properties") or {}
|
|
62
|
+
sent = (fn.get("description"), {p: d.get("description") if isinstance(d, dict) else None
|
|
63
|
+
for p, d in props.items()}, sorted(params.get("required") or []))
|
|
64
|
+
if sent != (mine["description"], mine["params"], sorted(mine["required"])):
|
|
65
|
+
return False
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class Matcher:
|
|
70
|
+
"""Checks units against versions; patterns are compiled once per version and node, and
|
|
71
|
+
an instruction text is matched once per version and node (steps repeat them)."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, versions):
|
|
74
|
+
self.versions = versions
|
|
75
|
+
self._cache = {}
|
|
76
|
+
self._instructions = {}
|
|
77
|
+
self._lines = None # {system text: its whole-lines pattern}, every version's
|
|
78
|
+
self._held = {} # {first system message: {system text: its spans there}}
|
|
79
|
+
|
|
80
|
+
def _inside(self, first):
|
|
81
|
+
"""{system text: its spans in `first` as whole lines that no other system text of the
|
|
82
|
+
versions holds as well}. A version that only added lines to the system text holds the
|
|
83
|
+
older text's lines, which then must not match its steps."""
|
|
84
|
+
if self._lines is None:
|
|
85
|
+
texts = {b.text for v in self.versions for b in [v.system] + [n.system for n in v.nodes.values()]
|
|
86
|
+
if b is not None}
|
|
87
|
+
self._lines = {t: re.compile(_WHOLE_LINES % fragment_pattern(t)[0]) for t in texts}
|
|
88
|
+
if first not in self._held:
|
|
89
|
+
found = {t: [m.span() for m in p.finditer(first)] for t, p in self._lines.items()}
|
|
90
|
+
spans = {s for f in found.values() for s in f}
|
|
91
|
+
self._held[first] = {t: [s for s in f if not any(o != s and o[0] <= s[0] and s[1] <= o[1] for o in spans)]
|
|
92
|
+
for t, f in found.items()}
|
|
93
|
+
return self._held[first]
|
|
94
|
+
|
|
95
|
+
def _compiled(self, version, node):
|
|
96
|
+
key = (version.label, node.name)
|
|
97
|
+
if key not in self._cache:
|
|
98
|
+
parts = [r"\s*"]
|
|
99
|
+
for i, block in enumerate(node.blocks):
|
|
100
|
+
p = "(?:%s)" % fragment_pattern(block.text, prefix=f"b{i}_")[0]
|
|
101
|
+
parts.append((p + "?" if block.conditional else p) + r"\s*")
|
|
102
|
+
systems = [fragment_pattern(b.text, prefix=f"s{i}_")[0]
|
|
103
|
+
for i, b in enumerate((node.system, version.system)) if b is not None]
|
|
104
|
+
system = re.compile("(?:%s)\\s*\\Z" % "|".join(systems)) if systems else None
|
|
105
|
+
self._cache[key] = (re.compile("".join(parts)), version.node_tools(node.name), system)
|
|
106
|
+
return self._cache[key]
|
|
107
|
+
|
|
108
|
+
def check(self, version, unit):
|
|
109
|
+
"""None when the version renders the unit, else the first check that fails."""
|
|
110
|
+
node = version.nodes.get(unit.get("node"))
|
|
111
|
+
if node is None:
|
|
112
|
+
return "node"
|
|
113
|
+
instruction, tools, system = self._compiled(version, node)
|
|
114
|
+
key = (version.label, node.name, unit_instruction(unit))
|
|
115
|
+
if key not in self._instructions:
|
|
116
|
+
self._instructions[key] = instruction.fullmatch(key[2]) is not None
|
|
117
|
+
if not self._instructions[key]:
|
|
118
|
+
return "instruction"
|
|
119
|
+
if not _tools_agree(unit, tools):
|
|
120
|
+
return "tools"
|
|
121
|
+
if system is not None:
|
|
122
|
+
messages = unit["request"]["messages"]
|
|
123
|
+
first = (messages[0].get("content") or "") if messages and messages[0].get("role") == "system" else ""
|
|
124
|
+
if not system.search(first) and not any(self._inside(first).get(b.text)
|
|
125
|
+
for b in (node.system, version.system) if b is not None):
|
|
126
|
+
return "system"
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _sha16(path):
|
|
131
|
+
with open(path, "rb") as f:
|
|
132
|
+
return hashlib.sha256(f.read()).hexdigest()[:16]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def attribute(units, versions):
|
|
136
|
+
"""Set compatible_versions and agent_version on every unit. Returns (the version set of
|
|
137
|
+
each session: None when no unit of it renders, empty on a conflict; the report)."""
|
|
138
|
+
matcher, labels = Matcher(versions), [v.label for v in versions]
|
|
139
|
+
unrendered = defaultdict(Counter) # node -> how far the closest version got
|
|
140
|
+
for u in units:
|
|
141
|
+
failed = {v.label: matcher.check(v, u) for v in versions}
|
|
142
|
+
u["compatible_versions"] = [label for label in labels if failed[label] is None]
|
|
143
|
+
if not u["compatible_versions"]:
|
|
144
|
+
unrendered[str(u.get("node"))][max(failed.values(), key=CHECKS.index)] += 1
|
|
145
|
+
by_session = defaultdict(list)
|
|
146
|
+
for u in units:
|
|
147
|
+
by_session[u.get("session_id")].append(u)
|
|
148
|
+
session_sets, session_counts, version_sessions = {}, Counter(), Counter()
|
|
149
|
+
for sid, members in by_session.items():
|
|
150
|
+
sets = [set(u["compatible_versions"]) for u in members if u["compatible_versions"]]
|
|
151
|
+
found = set.intersection(*sets) if sets else None
|
|
152
|
+
session_sets[sid] = found
|
|
153
|
+
only = next(iter(found)) if found and len(found) == 1 else None
|
|
154
|
+
for u in members:
|
|
155
|
+
u["agent_version"] = only
|
|
156
|
+
if found is None:
|
|
157
|
+
session_counts["unknown"] += 1
|
|
158
|
+
elif not found:
|
|
159
|
+
session_counts["conflict"] += 1
|
|
160
|
+
elif only:
|
|
161
|
+
session_counts["one_version"] += 1
|
|
162
|
+
version_sessions[only] += 1
|
|
163
|
+
else:
|
|
164
|
+
session_counts["several_versions"] += 1
|
|
165
|
+
rendered = Counter(label for u in units for label in u["compatible_versions"])
|
|
166
|
+
report = {
|
|
167
|
+
"schema": SCHEMA_VERSIONS,
|
|
168
|
+
"versions": [{"label": v.label, "target": v.target, "file": os.path.basename(v.path) if v.path else None,
|
|
169
|
+
"sha256": _sha16(v.path) if v.path else None, "units_rendered": rendered[v.label],
|
|
170
|
+
"sessions": version_sessions[v.label]} for v in versions],
|
|
171
|
+
"units": len(units),
|
|
172
|
+
"units_rendered": sum(1 for u in units if u["compatible_versions"]),
|
|
173
|
+
"units_not_rendered_by_node": {node: dict(c) for node, c in sorted(unrendered.items())},
|
|
174
|
+
"sessions": dict({"total": len(by_session)}, **{k: session_counts[k] for k in
|
|
175
|
+
("one_version", "several_versions", "conflict", "unknown")}),
|
|
176
|
+
"selected": None,
|
|
177
|
+
}
|
|
178
|
+
return session_sets, report
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def select(units, label, session_sets):
|
|
182
|
+
"""(the units of sessions that version `label` can have served, dropped counts by reason)."""
|
|
183
|
+
kept, dropped = [], Counter()
|
|
184
|
+
for u in units:
|
|
185
|
+
found = session_sets.get(u.get("session_id"))
|
|
186
|
+
if found is None:
|
|
187
|
+
dropped["unknown_version"] += 1
|
|
188
|
+
elif not found:
|
|
189
|
+
dropped["version_conflict"] += 1
|
|
190
|
+
elif label not in found:
|
|
191
|
+
dropped["other_version"] += 1
|
|
192
|
+
else:
|
|
193
|
+
kept.append(u)
|
|
194
|
+
return kept, dropped
|