mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-07-30 03:09:50 +08:00
131 lines
4.8 KiB
Python
131 lines
4.8 KiB
Python
"""NGA InvocationTarget ID-prefix router.
|
|
|
|
NGA bots store Salesforce Ids in GenAiFunctionDefinition.InvocationTarget,
|
|
not DeveloperNames. This module resolves the kind by the 3-char key prefix.
|
|
|
|
unknown prefixes MUST NEVER raise — Salesforce is free to introduce
|
|
new NGA target types (e.g., a hypothetical `0aN...` AgentCapability) and
|
|
the skill must degrade gracefully. Callers use `resolve_or_unresolved` to
|
|
append a `_unresolved[]` entry with a diagnostic reason and get back
|
|
`("unknown", "skip")` so the wave orchestrator can keep parsing.
|
|
|
|
Invariants:
|
|
* Only the 3-char prefix is consulted for Id-based routing — 15-char
|
|
and 18-char Ids both map the same way.
|
|
* `standard_action` kind is selected by caller context (bundle's
|
|
`invocationTargetType == "standardInvocableAction"`), not by prefix —
|
|
standard actions have no Id. Its presence in the Kind union is
|
|
scaffolding for call-site type-checking.
|
|
* `REGISTERED_PREFIXES` exposes the known prefix set as a frozenset for
|
|
tests and probe-channel validation.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Literal
|
|
|
|
Kind = Literal[
|
|
"apex",
|
|
"flow_definition",
|
|
"flow_version",
|
|
"prompt_template",
|
|
"standard_action",
|
|
"unknown",
|
|
]
|
|
Channel = Literal[
|
|
"tooling_soql",
|
|
"retrieve_required",
|
|
"skip",
|
|
]
|
|
|
|
_PREFIX_MAP: dict[str, tuple[Kind, Channel]] = {
|
|
"01p": ("apex", "tooling_soql"),
|
|
"300": ("flow_definition", "tooling_soql"),
|
|
"301": ("flow_version", "tooling_soql"),
|
|
"0hf": ("prompt_template", "retrieve_required"),
|
|
}
|
|
|
|
# public reverse-lookup set. `frozenset` (not set) so callers can't
|
|
# mutate the authoritative table by accident.
|
|
REGISTERED_PREFIXES: frozenset[str] = frozenset(_PREFIX_MAP.keys())
|
|
|
|
# Salesforce Ids are 15 or 18 chars, alphanumeric only. This is the gate
|
|
# between "could be a real Id with an unknown prefix" (treat as an NGA
|
|
# addition we don't know about yet) and "garbage input" (empty, wrong
|
|
# length, bad characters — treat as invalid format).
|
|
_SF_ID_RE = re.compile(r"^[A-Za-z0-9]+$")
|
|
_SF_ID_LENGTHS = frozenset({15, 18})
|
|
|
|
|
|
def looks_like_sf_id(id_str: object) -> bool:
|
|
"""True iff `id_str` is a 15- or 18-char alphanumeric string.
|
|
|
|
promoted from module-private `_looks_like_sf_id`
|
|
so `main._route` can gate calls to `resolve_or_unresolved` on the
|
|
Id shape without reaching into a leading-underscore symbol (same
|
|
surface). Underscore form retained as a deprecated alias.
|
|
"""
|
|
if not isinstance(id_str, str):
|
|
return False
|
|
if len(id_str) not in _SF_ID_LENGTHS:
|
|
return False
|
|
return bool(_SF_ID_RE.match(id_str))
|
|
|
|
|
|
# backcompat alias — deprecated; use `looks_like_sf_id`.
|
|
_looks_like_sf_id = looks_like_sf_id
|
|
|
|
|
|
def resolve_target_id(id_str: str) -> tuple[Kind, Channel]:
|
|
"""Return (kind, resolvable_via) for a Salesforce Id.
|
|
|
|
unknown prefixes NEVER raise — callers observing ("unknown", "skip")
|
|
are expected to route through `resolve_or_unresolved` (or equivalent)
|
|
to append an `_unresolved[]` entry. This function stays pure — it does
|
|
not mutate any caller state, does not log.
|
|
"""
|
|
if not id_str or not isinstance(id_str, str) or len(id_str) < 3:
|
|
return ("unknown", "skip")
|
|
prefix = id_str[:3]
|
|
return _PREFIX_MAP.get(prefix, ("unknown", "skip"))
|
|
|
|
|
|
def resolve_or_unresolved(
|
|
id_str: object,
|
|
unresolved: list[dict],
|
|
) -> tuple[Kind, Channel]:
|
|
"""Like `resolve_target_id` but records failures into `unresolved`.
|
|
|
|
semantics:
|
|
* Valid Salesforce Id (15 or 18 alphanumeric chars) with known prefix:
|
|
route as usual. `unresolved` untouched.
|
|
* Valid Id shape, unknown prefix: append
|
|
`{"id": <id>, "reason": f"unknown-id-prefix:{id[:3]}"}` and
|
|
return ("unknown", "skip"). This is the "NGA shipped a new type"
|
|
path — the wave orchestrator keeps running.
|
|
* Not an Id shape (empty/None/wrong length/bad chars): append
|
|
`{"id": str(id_str), "reason": "invalid-id-format"}` and return
|
|
("unknown", "skip"). Distinct reason so triage can tell "NGA
|
|
added something new" apart from "caller passed us garbage."
|
|
|
|
`unresolved` is mutated in place — same contract as `_unresolved[]`
|
|
throughout the skill. Caller owns the list.
|
|
"""
|
|
if _looks_like_sf_id(id_str):
|
|
kind, channel = resolve_target_id(id_str) # type: ignore[arg-type]
|
|
if kind == "unknown":
|
|
# Real Id shape, prefix we don't recognize — record the prefix
|
|
# so triage can add it to _PREFIX_MAP.
|
|
unresolved.append({
|
|
"id": id_str,
|
|
"reason": f"unknown-id-prefix:{id_str[:3]}", # type: ignore[index]
|
|
})
|
|
return (kind, channel)
|
|
|
|
# Not a valid Id shape — empty, None, wrong length, or bad chars.
|
|
unresolved.append({
|
|
"id": str(id_str),
|
|
"reason": "invalid-id-format",
|
|
})
|
|
return ("unknown", "skip")
|