@google-cloud/kcmd 0.0.0-stage → 0.1.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/LICENSE +203 -0
- package/README.md +266 -2
- package/build/ts/kcmd/cli.js +19578 -0
- package/build/ts/kcmd/gcp/alloydb.d.ts +12 -0
- package/build/ts/kcmd/gcp/alloydb.js +70 -0
- package/build/ts/kcmd/gcp/api.d.ts +29 -0
- package/build/ts/kcmd/gcp/api.js +162 -0
- package/build/ts/kcmd/gcp/biglake.d.ts +16 -0
- package/build/ts/kcmd/gcp/biglake.js +50 -0
- package/build/ts/kcmd/gcp/bigquery.d.ts +57 -0
- package/build/ts/kcmd/gcp/bigquery.js +134 -0
- package/build/ts/kcmd/gcp/context.d.ts +10 -0
- package/build/ts/kcmd/gcp/context.js +74 -0
- package/build/ts/kcmd/gcp/crm.d.ts +13 -0
- package/build/ts/kcmd/gcp/crm.js +91 -0
- package/build/ts/kcmd/gcp/dataplex.d.ts +147 -0
- package/build/ts/kcmd/gcp/dataplex.js +406 -0
- package/build/ts/kcmd/gcp/index.d.ts +3 -0
- package/build/ts/kcmd/gcp/index.js +21 -0
- package/build/ts/kcmd/gcp/spanner.d.ts +63 -0
- package/build/ts/kcmd/gcp/spanner.js +123 -0
- package/build/ts/kcmd/index.d.ts +7 -0
- package/build/ts/kcmd/index.js +46 -0
- package/build/ts/kcmd/layout.d.ts +16 -0
- package/build/ts/kcmd/layout.js +31 -0
- package/build/ts/kcmd/layouts/documents.d.ts +24 -0
- package/build/ts/kcmd/layouts/documents.js +264 -0
- package/build/ts/kcmd/layouts/okf.d.ts +31 -0
- package/build/ts/kcmd/layouts/okf.js +344 -0
- package/build/ts/kcmd/layouts/semantic-model.d.ts +61 -0
- package/build/ts/kcmd/layouts/semantic-model.js +542 -0
- package/build/ts/kcmd/layouts/standard.d.ts +13 -0
- package/build/ts/kcmd/layouts/standard.js +108 -0
- package/build/ts/kcmd/manifest.d.ts +34 -0
- package/build/ts/kcmd/manifest.js +262 -0
- package/build/ts/kcmd/metadata.d.ts +25 -0
- package/build/ts/kcmd/metadata.js +8 -0
- package/build/ts/kcmd/okf/catalog_residue.d.ts +14 -0
- package/build/ts/kcmd/okf/catalog_residue.js +124 -0
- package/build/ts/kcmd/okf/concept.d.ts +14 -0
- package/build/ts/kcmd/okf/concept.js +104 -0
- package/build/ts/kcmd/okf/concept_to_entry.d.ts +8 -0
- package/build/ts/kcmd/okf/concept_to_entry.js +254 -0
- package/build/ts/kcmd/okf/init_okf_workspace.d.ts +15 -0
- package/build/ts/kcmd/okf/init_okf_workspace.js +115 -0
- package/build/ts/kcmd/okf/links.d.ts +23 -0
- package/build/ts/kcmd/okf/links.js +468 -0
- package/build/ts/kcmd/okf/okf_metadata.d.ts +12 -0
- package/build/ts/kcmd/okf/okf_metadata.js +82 -0
- package/build/ts/kcmd/semantic/bigquery.d.ts +22 -0
- package/build/ts/kcmd/semantic/bigquery.js +525 -0
- package/build/ts/kcmd/semantic/binding.d.ts +3 -0
- package/build/ts/kcmd/semantic/binding.js +97 -0
- package/build/ts/kcmd/semantic/catalog_entry.d.ts +24 -0
- package/build/ts/kcmd/semantic/catalog_entry.js +35 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.d.ts +26 -0
- package/build/ts/kcmd/semantic/converters/owl/convert.js +41 -0
- package/build/ts/kcmd/semantic/converters/owl/model.d.ts +98 -0
- package/build/ts/kcmd/semantic/converters/owl/model.js +30 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.d.ts +11 -0
- package/build/ts/kcmd/semantic/converters/owl/parse.js +634 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.d.ts +19 -0
- package/build/ts/kcmd/semantic/converters/owl/to_ir.js +390 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.d.ts +20 -0
- package/build/ts/kcmd/semantic/deploy_bigquery.js +226 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.d.ts +129 -0
- package/build/ts/kcmd/semantic/deploy_knowledge_catalog.js +1396 -0
- package/build/ts/kcmd/semantic/deploy_spanner.d.ts +15 -0
- package/build/ts/kcmd/semantic/deploy_spanner.js +150 -0
- package/build/ts/kcmd/semantic/deployment_target.d.ts +93 -0
- package/build/ts/kcmd/semantic/deployment_target.js +379 -0
- package/build/ts/kcmd/semantic/graph_ddl.d.ts +34 -0
- package/build/ts/kcmd/semantic/graph_ddl.js +563 -0
- package/build/ts/kcmd/semantic/import_model.d.ts +50 -0
- package/build/ts/kcmd/semantic/import_model.js +1275 -0
- package/build/ts/kcmd/semantic/ir.d.ts +589 -0
- package/build/ts/kcmd/semantic/ir.js +247 -0
- package/build/ts/kcmd/semantic/kc_actions.d.ts +45 -0
- package/build/ts/kcmd/semantic/kc_actions.js +531 -0
- package/build/ts/kcmd/semantic/kc_constraints.d.ts +44 -0
- package/build/ts/kcmd/semantic/kc_constraints.js +279 -0
- package/build/ts/kcmd/semantic/kc_converter.d.ts +31 -0
- package/build/ts/kcmd/semantic/kc_converter.js +824 -0
- package/build/ts/kcmd/semantic/kc_custom_types.d.ts +70 -0
- package/build/ts/kcmd/semantic/kc_custom_types.js +828 -0
- package/build/ts/kcmd/semantic/kc_entries.d.ts +155 -0
- package/build/ts/kcmd/semantic/kc_entries.js +284 -0
- package/build/ts/kcmd/semantic/kc_ids.d.ts +103 -0
- package/build/ts/kcmd/semantic/kc_ids.js +191 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.d.ts +32 -0
- package/build/ts/kcmd/semantic/knowledge_catalog.js +1043 -0
- package/build/ts/kcmd/semantic/loader.d.ts +262 -0
- package/build/ts/kcmd/semantic/loader.js +3440 -0
- package/build/ts/kcmd/semantic/names.d.ts +10 -0
- package/build/ts/kcmd/semantic/names.js +85 -0
- package/build/ts/kcmd/semantic/osi_converter.d.ts +60 -0
- package/build/ts/kcmd/semantic/osi_converter.js +911 -0
- package/build/ts/kcmd/semantic/parameters.d.ts +18 -0
- package/build/ts/kcmd/semantic/parameters.js +152 -0
- package/build/ts/kcmd/semantic/pull_kc.d.ts +14 -0
- package/build/ts/kcmd/semantic/pull_kc.js +346 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.d.ts +54 -0
- package/build/ts/kcmd/semantic/resolve_inheritance.js +330 -0
- package/build/ts/kcmd/semantic/resolve_profiles.d.ts +250 -0
- package/build/ts/kcmd/semantic/resolve_profiles.js +1822 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.d.ts +93 -0
- package/build/ts/kcmd/semantic/runtime/agent_tools.js +243 -0
- package/build/ts/kcmd/semantic/runtime/dialect.d.ts +13 -0
- package/build/ts/kcmd/semantic/runtime/dialect.js +39 -0
- package/build/ts/kcmd/semantic/runtime/runtime.d.ts +94 -0
- package/build/ts/kcmd/semantic/runtime/runtime.js +318 -0
- package/build/ts/kcmd/semantic/runtime/store.d.ts +112 -0
- package/build/ts/kcmd/semantic/runtime/store.js +344 -0
- package/build/ts/kcmd/semantic/skills.d.ts +110 -0
- package/build/ts/kcmd/semantic/skills.js +697 -0
- package/build/ts/kcmd/semantic/source_dialect.d.ts +10 -0
- package/build/ts/kcmd/semantic/source_dialect.js +261 -0
- package/build/ts/kcmd/semantic/source_entries.d.ts +12 -0
- package/build/ts/kcmd/semantic/source_entries.js +282 -0
- package/build/ts/kcmd/semantic/spanner.d.ts +18 -0
- package/build/ts/kcmd/semantic/spanner.js +60 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.d.ts +17 -0
- package/build/ts/kcmd/semantic/sql_expr_utils.js +221 -0
- package/build/ts/kcmd/semantic/sql_identifiers.d.ts +6 -0
- package/build/ts/kcmd/semantic/sql_identifiers.js +267 -0
- package/build/ts/kcmd/semantic/sql_parser.d.ts +44 -0
- package/build/ts/kcmd/semantic/sql_parser.js +378 -0
- package/build/ts/kcmd/semantic/transpile.d.ts +74 -0
- package/build/ts/kcmd/semantic/transpile.js +563 -0
- package/build/ts/kcmd/semantic/validate.d.ts +35 -0
- package/build/ts/kcmd/semantic/validate.js +1899 -0
- package/build/ts/kcmd/semantic/yaml_options.d.ts +6 -0
- package/build/ts/kcmd/semantic/yaml_options.js +58 -0
- package/build/ts/kcmd/semantic/yaml_update.d.ts +26 -0
- package/build/ts/kcmd/semantic/yaml_update.js +782 -0
- package/build/ts/kcmd/snapshot.d.ts +33 -0
- package/build/ts/kcmd/snapshot.js +379 -0
- package/build/ts/kcmd/source.d.ts +31 -0
- package/build/ts/kcmd/source.js +119 -0
- package/build/ts/kcmd/sources/bq-dataset.d.ts +17 -0
- package/build/ts/kcmd/sources/bq-dataset.js +131 -0
- package/build/ts/kcmd/sources/entrygroup.d.ts +21 -0
- package/build/ts/kcmd/sources/entrygroup.js +83 -0
- package/build/ts/kcmd/sources/kb.d.ts +21 -0
- package/build/ts/kcmd/sources/kb.js +82 -0
- package/build/ts/kcmd/sources/semantic-model.d.ts +18 -0
- package/build/ts/kcmd/sources/semantic-model.js +48 -0
- package/build/ts/kcmd/sync.d.ts +35 -0
- package/build/ts/kcmd/sync.js +697 -0
- package/package.json +62 -4
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The store a model's data lives in.
|
|
3
|
+
//
|
|
4
|
+
// A deployment that uses a binding profile names a target; this module turns
|
|
5
|
+
// that declaration into structured store coordinates. A profile may deploy to
|
|
6
|
+
// Spanner, to AlloyDB or to BigQuery. `kind` is the discriminant, and what
|
|
7
|
+
// differs by it is how the store is ADDRESSED -- the coordinates it takes and
|
|
8
|
+
// the dialect it speaks -- not whether an action may write to it.
|
|
9
|
+
//
|
|
10
|
+
// All three execute SQL DML, so all three can be the store a model's actions
|
|
11
|
+
// run against. BigQuery is a warehouse rather than a transactional database
|
|
12
|
+
// and a write there costs what a warehouse write costs, but that is a question
|
|
13
|
+
// about the model an author is building, not a rule this module enforces.
|
|
14
|
+
//
|
|
15
|
+
// Identity is `name`, the resource the store addresses. Two profiles naming
|
|
16
|
+
// the same database describe ONE store; the profile is how a caller found it,
|
|
17
|
+
// not what it is.
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
+
exports.resolveStore = resolveStore;
|
|
20
|
+
exports.profileTargets = profileTargets;
|
|
21
|
+
exports.withPrimaryInstance = withPrimaryInstance;
|
|
22
|
+
exports.storeLine = storeLine;
|
|
23
|
+
const deployment_target_1 = require("../deployment_target");
|
|
24
|
+
const loader_1 = require("../loader");
|
|
25
|
+
const resolve_profiles_1 = require("../resolve_profiles");
|
|
26
|
+
// A BigQuery table an entity is bound to. The loader rewrites the resource-name
|
|
27
|
+
// URI an author may write into `project.dataset.table`, which is the form the
|
|
28
|
+
// generator emits and the only one that reaches here. The table name takes any
|
|
29
|
+
// character the loader allows in one, which is anything but a dot or a slash,
|
|
30
|
+
// so a name with `$` or a letter outside ASCII is still a table here.
|
|
31
|
+
const BIGQUERY_TABLE_SOURCE = /^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)\.[^./]+$/;
|
|
32
|
+
// The database a bound table sits in, or undefined when the source names no
|
|
33
|
+
// table of any backend this module knows. A Spanner or AlloyDB source, in
|
|
34
|
+
// either spelling, takes the loader's databaseOf form, `spanner/<p>/<i>/<db>`
|
|
35
|
+
// or `alloydb/<p>/<l>/<c>/<db>`.
|
|
36
|
+
function storeOf(source) {
|
|
37
|
+
const database = (0, loader_1.databaseOf)(source);
|
|
38
|
+
if (database?.startsWith('spanner/') || database?.startsWith('alloydb/')) {
|
|
39
|
+
return database;
|
|
40
|
+
}
|
|
41
|
+
const bigQuery = source.match(BIGQUERY_TABLE_SOURCE);
|
|
42
|
+
if (bigQuery)
|
|
43
|
+
return `bigquery/${bigQuery[1]}/${bigQuery[2]}`;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
// Every table the model binds, not just its entities: a many-to-many
|
|
47
|
+
// relationship is backed by a junction table of its own, and an action that
|
|
48
|
+
// creates the edge writes to exactly that one.
|
|
49
|
+
function boundTables(model) {
|
|
50
|
+
const bindings = [];
|
|
51
|
+
for (const entity of model.entities ?? []) {
|
|
52
|
+
bindings.push({
|
|
53
|
+
name: entity.name,
|
|
54
|
+
source: entity.dataSource ?? '',
|
|
55
|
+
shown: (0, loader_1.writtenSource)(entity),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
for (const relationship of model.relationships ?? []) {
|
|
59
|
+
const source = relationship.association?.dataSource;
|
|
60
|
+
if (source) {
|
|
61
|
+
bindings.push({ name: relationship.name, source, shown: (0, loader_1.asResourceName)(source) });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return bindings;
|
|
65
|
+
}
|
|
66
|
+
// The database a store addresses, in the form storeOf gives. An AlloyDB
|
|
67
|
+
// store matches a source on project, location, cluster and database.
|
|
68
|
+
function storeDatabase(store) {
|
|
69
|
+
switch (store.kind) {
|
|
70
|
+
case 'spanner':
|
|
71
|
+
return `spanner/${store.project}/${store.instance}/${store.database}`;
|
|
72
|
+
case 'alloydb':
|
|
73
|
+
return `alloydb/${store.project}/${store.location}/${store.cluster}/${store.database}`;
|
|
74
|
+
case 'bigquery':
|
|
75
|
+
return `bigquery/${store.project}/${store.dataset}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Bindings that address something other than the store the deployment target
|
|
79
|
+
// names. An action's statements address a table by its NAME, with the
|
|
80
|
+
// project/instance/database qualifier dropped, so an entity bound elsewhere
|
|
81
|
+
// would produce a statement against whatever table of that name the target
|
|
82
|
+
// store happens to hold.
|
|
83
|
+
function strayBindings(model, store) {
|
|
84
|
+
const database = storeDatabase(store);
|
|
85
|
+
const strays = [];
|
|
86
|
+
for (const binding of boundTables(model)) {
|
|
87
|
+
const source = binding.source.trim();
|
|
88
|
+
if (!source)
|
|
89
|
+
continue;
|
|
90
|
+
const bound = storeOf(source);
|
|
91
|
+
if (!bound) {
|
|
92
|
+
strays.push(`'${binding.name}' to ${binding.shown}, which is not a table in this store`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (bound !== database)
|
|
96
|
+
strays.push(`'${binding.name}' to ${binding.shown}`);
|
|
97
|
+
}
|
|
98
|
+
return strays;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The store a model runs against under binding profile `profile`, which
|
|
102
|
+
* defaults to `default`, the model file's own bindings. It is the one the
|
|
103
|
+
* deployments for that profile name: those that name it in `profile`, or that
|
|
104
|
+
* name none or `default` for `default`. A model with no `deployments` list
|
|
105
|
+
* names no profile for any of its targets, so they are all for `default`.
|
|
106
|
+
* With several targets, the store is the single operational one, Spanner or
|
|
107
|
+
* AlloyDB.
|
|
108
|
+
*
|
|
109
|
+
* Fails six ways:
|
|
110
|
+
*
|
|
111
|
+
* 1. A GOOGLE block's data is not valid JSON, so its targets cannot be read.
|
|
112
|
+
* 2. A target names no supported destination. Push checks the target of every
|
|
113
|
+
* deployment, so this fails the store under every profile.
|
|
114
|
+
* 3. No deployment uses the profile.
|
|
115
|
+
* 4. There are several targets and no single operational one, so which store
|
|
116
|
+
* is meant is ambiguous.
|
|
117
|
+
* 5. There is no target, so there is nothing to run against.
|
|
118
|
+
* 6. The model binds a table outside the store. For a Spanner or AlloyDB
|
|
119
|
+
* store, a write would land in the wrong place, and the error is named. A
|
|
120
|
+
* BigQuery graph may read tables in any dataset, so
|
|
121
|
+
* for a BigQuery store the error is not named.
|
|
122
|
+
*/
|
|
123
|
+
function resolveStore(model, profile = resolve_profiles_1.DEFAULT_PROFILE) {
|
|
124
|
+
// Push checks the target of every deployment the model declares, so the
|
|
125
|
+
// targets of every deployment are read before the profile's are picked.
|
|
126
|
+
let all;
|
|
127
|
+
try {
|
|
128
|
+
all = (0, deployment_target_1.googleDeploymentTargets)(model);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
// It throws when a GOOGLE block's data is not valid JSON. Push reports
|
|
132
|
+
// that as an error, so the store fails.
|
|
133
|
+
return { error: `${err?.message || err}`, named: true, rejected: true };
|
|
134
|
+
}
|
|
135
|
+
// The loader reads a vanilla model's `deployments` out of its GOOGLE block,
|
|
136
|
+
// so a message names the block's `deployments`, as push does.
|
|
137
|
+
const vanilla = model.version === loader_1.OSSIE_VERSION;
|
|
138
|
+
const where = vanilla && model.deployments ?
|
|
139
|
+
`its GOOGLE block's 'deployments'` :
|
|
140
|
+
`'deployments'`;
|
|
141
|
+
// A target in no accepted form is reported, alone or beside one that gives
|
|
142
|
+
// a store, whichever profile its deployment is for.
|
|
143
|
+
if (all.malformed.length) {
|
|
144
|
+
const unparsed = model.deployments?.find(d => d.target === all.malformed[0]);
|
|
145
|
+
const error = unparsed ? (0, deployment_target_1.deploymentTargetError)(model, unparsed) :
|
|
146
|
+
(0, deployment_target_1.googleBlockTargetError)(model, all.malformed[0]);
|
|
147
|
+
return { error, named: true, rejected: true };
|
|
148
|
+
}
|
|
149
|
+
const used = deploymentsFor(model, profile);
|
|
150
|
+
if ('error' in used)
|
|
151
|
+
return used;
|
|
152
|
+
const scoped = used.model;
|
|
153
|
+
// The profile's targets are among those read above. So this does not
|
|
154
|
+
// throw, and none of them is in no accepted form.
|
|
155
|
+
const targets = (0, deployment_target_1.googleDeploymentTargets)(scoped);
|
|
156
|
+
const { spanner, alloyDb, bigQuery } = targets;
|
|
157
|
+
const declared = spanner.length + alloyDb.length + bigQuery.length;
|
|
158
|
+
const operational = [...spanner, ...alloyDb];
|
|
159
|
+
const contenders = operational.length ? operational : bigQuery;
|
|
160
|
+
if (contenders.length > 1) {
|
|
161
|
+
const counted = [
|
|
162
|
+
...(spanner.length ? [`${spanner.length} Spanner`] : []),
|
|
163
|
+
...(alloyDb.length ? [`${alloyDb.length} AlloyDB`] : []),
|
|
164
|
+
...(operational.length ? [] : [`${bigQuery.length} BigQuery`]),
|
|
165
|
+
];
|
|
166
|
+
const what = counted.length > 1 ?
|
|
167
|
+
`deployment targets on two operational backends (${counted.join(' and ')})` :
|
|
168
|
+
`${counted[0]} deployment targets`;
|
|
169
|
+
const names = deploymentNames(scoped, contenders.map(t => t.uri), profile);
|
|
170
|
+
return {
|
|
171
|
+
error: `Model '${model.name}' declares ${what} in ${where}${names}, so which store it runs against is ambiguous; the ` +
|
|
172
|
+
`runtime uses exactly one deployment for now.`,
|
|
173
|
+
named: true,
|
|
174
|
+
rejected: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (!declared) {
|
|
178
|
+
const declare = vanilla ?
|
|
179
|
+
` In a GOOGLE custom_extensions block on the model, declare a ` +
|
|
180
|
+
`deployment in 'deployments' whose target names a database.` :
|
|
181
|
+
` Declare a deployment in 'deployments' whose target names a ` +
|
|
182
|
+
`database.`;
|
|
183
|
+
return {
|
|
184
|
+
error: `Model '${model.name}' declares no deployment target in ${where}, so there is no store to run against.${declare}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const store = storeFor(spanner[0], alloyDb[0], bigQuery[0]);
|
|
188
|
+
const strays = strayBindings(model, store);
|
|
189
|
+
if (strays.length) {
|
|
190
|
+
return {
|
|
191
|
+
error: `Model '${model.name}' binds ${strays.join(', ')}, but its ` +
|
|
192
|
+
`deployment target is ${store.name}. An action's statements ` +
|
|
193
|
+
`address a table by name alone, so the write would land in the ` +
|
|
194
|
+
`target store's table of that name rather than in the bound one. ` +
|
|
195
|
+
`Bind both to the same store.`,
|
|
196
|
+
// A BigQuery graph may read tables in any dataset, BigLake included,
|
|
197
|
+
// so the model then has no store, and a caller
|
|
198
|
+
// does not fail.
|
|
199
|
+
...(store.kind === 'bigquery' ? {} : { named: true }),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return store;
|
|
203
|
+
}
|
|
204
|
+
// The model with only the deployments `profile` uses, or why there are none.
|
|
205
|
+
// A model with no `deployments` list keeps every target in its GOOGLE block
|
|
206
|
+
// for `default`. An empty list names no deployment. Either way, any other
|
|
207
|
+
// profile has none.
|
|
208
|
+
function deploymentsFor(model, profile) {
|
|
209
|
+
const nothingToRun = `so there is no database to run it against`;
|
|
210
|
+
const namedProfile = `binding profile '${profile}' is a named profile, ` +
|
|
211
|
+
`which names no deployment target`;
|
|
212
|
+
if (!model.deployments?.length) {
|
|
213
|
+
return profile === resolve_profiles_1.DEFAULT_PROFILE ?
|
|
214
|
+
{ model } :
|
|
215
|
+
{ error: `${namedProfile}, ${nothingToRun}` };
|
|
216
|
+
}
|
|
217
|
+
const used = model.deployments.filter(d => (d.profile ?? resolve_profiles_1.DEFAULT_PROFILE) === profile);
|
|
218
|
+
if (!used.length) {
|
|
219
|
+
// A vanilla model lists its deployments in its GOOGLE block.
|
|
220
|
+
const listed = model.version === loader_1.OSSIE_VERSION ?
|
|
221
|
+
`its GOOGLE block's 'deployments'` :
|
|
222
|
+
`'deployments'`;
|
|
223
|
+
let error;
|
|
224
|
+
if (profile === resolve_profiles_1.DEFAULT_PROFILE) {
|
|
225
|
+
error = `Model '${model.name}' declares no deployment without a ` +
|
|
226
|
+
`'profile' in ${listed}, so there is no store to run against ` +
|
|
227
|
+
`under binding profile '${resolve_profiles_1.DEFAULT_PROFILE}'.`;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
error = `${namedProfile}, and no deployment in ${listed} uses it, ${nothingToRun}`;
|
|
231
|
+
}
|
|
232
|
+
return { error };
|
|
233
|
+
}
|
|
234
|
+
return { model: { ...model, deployments: used } };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The targets binding profile `profile` uses, as resolveStore reads them: its
|
|
238
|
+
* deployments' targets, or every target in a GOOGLE block for `default` when
|
|
239
|
+
* the model has no `deployments` list. Empty when no deployment is for the
|
|
240
|
+
* profile. Throws when a GOOGLE block's data is not valid JSON.
|
|
241
|
+
*/
|
|
242
|
+
function profileTargets(model, profile) {
|
|
243
|
+
const used = deploymentsFor(model, profile);
|
|
244
|
+
return 'error' in used ? [] : (0, deployment_target_1.googleDeploymentTargets)(used.model).uris;
|
|
245
|
+
}
|
|
246
|
+
// The deployments whose targets are `uris`, for a message, or nothing when
|
|
247
|
+
// the targets come from a GOOGLE block, which names none. `profile` is the
|
|
248
|
+
// profile they are for.
|
|
249
|
+
function deploymentNames(model, uris, profile) {
|
|
250
|
+
const deployments = model.deployments;
|
|
251
|
+
if (!deployments)
|
|
252
|
+
return '';
|
|
253
|
+
const names = uris.map(uri => deployments.find(d => d.target === uri)?.name ?? uri);
|
|
254
|
+
return ` for profile '${profile}' (${names.join(', ')})`;
|
|
255
|
+
}
|
|
256
|
+
function storeFor(spanner, alloyDb, bigQuery) {
|
|
257
|
+
if (spanner) {
|
|
258
|
+
return {
|
|
259
|
+
kind: 'spanner',
|
|
260
|
+
name: `projects/${spanner.project}/instances/${spanner.instance}/databases/${spanner.database}`,
|
|
261
|
+
project: spanner.project,
|
|
262
|
+
instance: spanner.instance,
|
|
263
|
+
database: spanner.database,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (alloyDb) {
|
|
267
|
+
return {
|
|
268
|
+
kind: 'alloydb',
|
|
269
|
+
name: `projects/${alloyDb.project}/locations/${alloyDb.location}/clusters/${alloyDb.cluster}/databases/${alloyDb.database}`,
|
|
270
|
+
project: alloyDb.project,
|
|
271
|
+
location: alloyDb.location,
|
|
272
|
+
cluster: alloyDb.cluster,
|
|
273
|
+
database: alloyDb.database,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
kind: 'bigquery',
|
|
278
|
+
name: `projects/${bigQuery.project}/datasets/${bigQuery.dataset}`,
|
|
279
|
+
project: bigQuery.project,
|
|
280
|
+
dataset: bigQuery.dataset,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* `store` with the instance a connection goes through. An AlloyDB target
|
|
285
|
+
* names a database in a cluster, so this asks the AlloyDB API for the
|
|
286
|
+
* cluster's primary instance. Fails, naming the cluster, when it has none,
|
|
287
|
+
* when the primary is not READY, or when the listing fails. Any other store
|
|
288
|
+
* comes back unchanged.
|
|
289
|
+
*/
|
|
290
|
+
async function withPrimaryInstance(store, client) {
|
|
291
|
+
if (store.kind !== 'alloydb')
|
|
292
|
+
return store;
|
|
293
|
+
const cluster = `projects/${store.project}/locations/${store.location}/clusters/${store.cluster}`;
|
|
294
|
+
let res;
|
|
295
|
+
try {
|
|
296
|
+
res = await client.listInstances(store.project, store.location, store.cluster);
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
return {
|
|
300
|
+
error: `Could not list the instances of AlloyDB cluster '${cluster}': ${err?.message ?? err}`,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (res.status !== 200) {
|
|
304
|
+
return {
|
|
305
|
+
error: `Could not list the instances of AlloyDB cluster '${cluster}': ${apiError(res)}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const primary = res.result?.find(i => i.instanceType === 'PRIMARY');
|
|
309
|
+
const instance = primary?.name?.split('/').pop();
|
|
310
|
+
if (!primary || !instance) {
|
|
311
|
+
return {
|
|
312
|
+
error: `AlloyDB cluster '${cluster}' has no primary instance, so there ` +
|
|
313
|
+
`is no instance to connect to database '${store.database}' through.`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (primary.state !== 'READY') {
|
|
317
|
+
return {
|
|
318
|
+
error: `AlloyDB cluster '${cluster}' has primary instance '${instance}' in state ${primary.state ?? 'STATE_UNSPECIFIED'}; it ` +
|
|
319
|
+
`must be READY to connect to database '${store.database}' through it.`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
return { ...store, instance };
|
|
323
|
+
}
|
|
324
|
+
// The message of a failed API call, after the HTTP status.
|
|
325
|
+
function apiError(res) {
|
|
326
|
+
const message = res.message?.trim();
|
|
327
|
+
return message ? `${res.status} ${message}` : `${res.status}`;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* How a store is written down for a reader: the resource it addresses, with
|
|
331
|
+
* the backend named ahead of it for everything but Spanner, which is the one
|
|
332
|
+
* an unprefixed line has always meant.
|
|
333
|
+
*/
|
|
334
|
+
function storeLine(store) {
|
|
335
|
+
switch (store.kind) {
|
|
336
|
+
case 'spanner':
|
|
337
|
+
return `${store.project}/${store.instance}/${store.database}`;
|
|
338
|
+
case 'alloydb':
|
|
339
|
+
return `alloydb:${store.project}/${store.location}/${store.cluster}/` +
|
|
340
|
+
`${store.instance}/${store.database}`;
|
|
341
|
+
case 'bigquery':
|
|
342
|
+
return `bigquery:${store.project}/${store.dataset}`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generating an Agent Skill from a semantic model.
|
|
3
|
+
*
|
|
4
|
+
* An Agent Skill (agentskills.io) is a directory holding a `SKILL.md` -- YAML
|
|
5
|
+
* frontmatter naming the skill, then Markdown telling an agent how to do
|
|
6
|
+
* something. Clients load the frontmatter at startup and the body only when
|
|
7
|
+
* the skill looks relevant, so the body has a budget: under 500 lines, and
|
|
8
|
+
* under about 5,000 tokens. Anything longer goes in files the body points at,
|
|
9
|
+
* which the agent reads only if it needs them.
|
|
10
|
+
*
|
|
11
|
+
* That budget decides the shape of what this emits. A model's actions carry
|
|
12
|
+
* their parameters, the rules that gate them and the blast radius of each
|
|
13
|
+
* call, and inlining all of it puts a page of detail about every action in
|
|
14
|
+
* front of an agent that wanted one of them. So `SKILL.md` is a ROUTER: what
|
|
15
|
+
* the model is, a one-line table of the actions, and the parts that are true
|
|
16
|
+
* of every call. Each action's detail is a file under `references/`, named in
|
|
17
|
+
* the table, read on demand. A model with thirty actions costs the same at
|
|
18
|
+
* startup as a model with one.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here decides what an action means. `modelTools` already turns a
|
|
21
|
+
* bound model into the action descriptors a skill needs -- the tool name, the
|
|
22
|
+
* description with the gating rules folded in, the typed parameters, and
|
|
23
|
+
* whether this binding can run it -- so this module renders what `modelTools`
|
|
24
|
+
* returns and derives nothing of its own beyond layout.
|
|
25
|
+
*
|
|
26
|
+
* The skill is written against the LOGICAL model, which is what makes it worth
|
|
27
|
+
* generating once. An action's name, arguments, rules and blast radius are the
|
|
28
|
+
* same wherever it is deployed, because an executor is a physical binding that
|
|
29
|
+
* a profile supplies. So every `references/` page is a fact about the model
|
|
30
|
+
* and nothing else, and everything that reads the binding lives in `SKILL.md`:
|
|
31
|
+
* the store, the executor kinds, which actions this deployment cannot run and
|
|
32
|
+
* why, and the command line to try one with, all gathered under "Running an
|
|
33
|
+
* action", plus the snippet under "Finding a record" that reads the store
|
|
34
|
+
* directly, which needs the store's kind and its coordinates. Point this at a
|
|
35
|
+
* different profile and those move and the reference pages do not.
|
|
36
|
+
*
|
|
37
|
+
* Keeping that true takes some discipline: whether an action is RUNNABLE is a
|
|
38
|
+
* binding fact wearing a logical name, and putting `tool.unavailable` on the
|
|
39
|
+
* action's own page -- which reads naturally, and which this module did at
|
|
40
|
+
* first -- quietly makes every page profile-specific.
|
|
41
|
+
*/
|
|
42
|
+
import { SemanticRuntime } from './runtime/runtime';
|
|
43
|
+
/** One file of the generated package, at a path relative to the skill root. */
|
|
44
|
+
export interface SkillFile {
|
|
45
|
+
/** Relative to the skill directory, POSIX-separated. `SKILL.md` is first. */
|
|
46
|
+
path: string;
|
|
47
|
+
text: string;
|
|
48
|
+
}
|
|
49
|
+
/** A generated skill: the directory name it must be written under, and why. */
|
|
50
|
+
export interface SkillPackage {
|
|
51
|
+
/**
|
|
52
|
+
* The skill's name, which is also the directory name it must be written
|
|
53
|
+
* under. The two are required to match, so they come from one place.
|
|
54
|
+
*/
|
|
55
|
+
name: string;
|
|
56
|
+
files: SkillFile[];
|
|
57
|
+
/**
|
|
58
|
+
* What a reader should know about the skill that was generated -- a body
|
|
59
|
+
* over its budget, a model that declares nothing to act on. Not errors:
|
|
60
|
+
* every one of them still produces a skill, and the caller decides whether
|
|
61
|
+
* it is the skill it wanted.
|
|
62
|
+
*/
|
|
63
|
+
warnings: string[];
|
|
64
|
+
}
|
|
65
|
+
export interface GenerateSkillOptions {
|
|
66
|
+
/** The model to describe, under the binding profile it was read with. */
|
|
67
|
+
runtime: SemanticRuntime;
|
|
68
|
+
/**
|
|
69
|
+
* The skill's name, defaulting to the model's own in the form the spec
|
|
70
|
+
* requires. Validated either way: a name the spec rejects is an error here
|
|
71
|
+
* rather than a skill some clients silently skip.
|
|
72
|
+
*/
|
|
73
|
+
name?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The skill for one model, as files a caller writes out.
|
|
77
|
+
*
|
|
78
|
+
* Returns an error for a name the Agent Skills spec would reject. It also
|
|
79
|
+
* returns the store error when the model's deployments are rejected, and
|
|
80
|
+
* when the profile names a store that cannot be resolved for a model with
|
|
81
|
+
* actions, whose skill names the store. A model with no actions is not an
|
|
82
|
+
* error -- it is a skill that says the model declares nothing to act on, which
|
|
83
|
+
* is the honest thing to hand an agent -- but it is warned about, because it
|
|
84
|
+
* is rarely what the caller meant.
|
|
85
|
+
*/
|
|
86
|
+
export declare function generateSkill(opts: GenerateSkillOptions): SkillPackage | {
|
|
87
|
+
error: string;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* A model name in the form the spec requires: 1-64 characters of lowercase
|
|
91
|
+
* `a-z0-9-`, with no leading, trailing or doubled hyphen.
|
|
92
|
+
*
|
|
93
|
+
* `Commerce` and `commerce_demo` are both ordinary model names and neither is
|
|
94
|
+
* a legal skill name, so this is a conversion rather than a check. What it
|
|
95
|
+
* cannot fix -- a name with no alphanumeric character in it at all -- falls
|
|
96
|
+
* through to the validator, which says so.
|
|
97
|
+
*/
|
|
98
|
+
export declare function skillNameFor(modelName: string): string;
|
|
99
|
+
/**
|
|
100
|
+
* Why this name is not a legal skill name, or undefined if it is.
|
|
101
|
+
*
|
|
102
|
+
* Worth being strict about here rather than leaving to the client. A skill
|
|
103
|
+
* whose `name` does not match its directory is loaded by a lenient client and
|
|
104
|
+
* SKIPPED by a strict one -- the Agent Plugins spec requires a client to skip
|
|
105
|
+
* a non-conformant skill -- so the failure shows up as a skill that quietly
|
|
106
|
+
* does nothing on somebody else's machine. This generator names the directory
|
|
107
|
+
* from the same string it writes into the frontmatter, so the pair cannot
|
|
108
|
+
* disagree; the rest is the character rules.
|
|
109
|
+
*/
|
|
110
|
+
export declare function whyNameIsInvalid(name: string): string | undefined;
|