@aglyn/plugins-ai 1.0.0-beta.238 → 1.0.0-beta.240
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/package.json +17 -17
- package/src/lib/components/ai-job-links.js +8 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +5 -0
- package/src/lib/components/ai-job-requests.js +4 -2
- package/src/lib/components/ai-job-requests.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +12 -0
- package/src/lib/components/ai-jobs-store.js +15 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
- package/src/lib/components/ai-usage-meter-refresh.js +63 -0
- package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
- package/src/lib/components/assist-panel.component.js +17 -1
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
- package/src/lib/components/use-ai-usage-meter.js +8 -3
- package/src/lib/components/use-ai-usage-meter.js.map +1 -1
- package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
- package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
- package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +29 -2
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-draft-ids.js +7 -2
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
- package/src/lib/jobs/ai-job-form-dataset.js +161 -0
- package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
- package/src/lib/jobs/ai-job-form-step.js +37 -9
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.js +7 -2
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
- package/src/lib/jobs/ai-job-page-language.js +44 -4
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.js +13 -2
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +32 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +18 -0
- package/src/lib/jobs/ai-job-plan-step.js +41 -4
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-datasets.d.ts +183 -0
- package/src/lib/jobs/ai-job-site-datasets.js +530 -0
- package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
- package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
- package/src/lib/jobs/ai-job-site-step.js +148 -29
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
- package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +10 -0
- package/src/lib/layout-language/ai-layout-compiler.js +93 -1
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-gaps.js +17 -1
- package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +32 -3
- package/src/lib/layout-language/ai-layout-listings.js +62 -9
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.js +12 -4
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/model/ai-build-job.d.ts +2 -0
- package/src/lib/model/ai-build-job.js +16 -1
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +23 -3
- package/src/lib/model/ai-build-plan.js +47 -6
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +19 -2
- package/src/lib/model/ai-site-job.js +21 -6
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
- package/src/lib/runtime/ai-dataset-generation.js +127 -0
- package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +84 -0
- package/src/lib/runtime/ai-doctrine-validators.js +307 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +10 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.d.ts +2 -0
- package/src/lib/runtime/ai-node-tree.js +16 -0
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
- package/src/lib/tools/ai-dataset-tool.js +231 -0
- package/src/lib/tools/ai-dataset-tool.js.map +1 -0
- package/src/lib/usage/free-ai-credits-left.js +26 -2
- package/src/lib/usage/free-ai-credits-left.js.map +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
18
|
+
import { collectFormFieldNodeIds, FORM_COMPONENT_ID, isMarketingConsentFieldName } from "@aglyn/aglyn/app-utils/forms";
|
|
19
|
+
import { pluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
20
|
+
import { AI_DATASET_DRAFT_RESOURCE } from "./ai-build-unit-outcome.js";
|
|
21
|
+
/**
|
|
22
|
+
* A FORM THAT WRITES TO A DATASET (AGL-3616; Zach, 2026-10-10: Aglyn AI's
|
|
23
|
+
* forms can write to datasets — a volunteer sign-up into a Volunteers
|
|
24
|
+
* dataset, an RSVP into an event's attendees, a catering inquiry into
|
|
25
|
+
* Inquiries).
|
|
26
|
+
*
|
|
27
|
+
* The plan says which dataset a form writes to (`writesTo` on its creation);
|
|
28
|
+
* it is never inferred from what a section places. The dataset is built
|
|
29
|
+
* first — a site start's dataset unit, a build's dataset item, or one the
|
|
30
|
+
* site already has — and the form step binds the form it generated the way a
|
|
31
|
+
* person binds one on the Form element: the form node's `datasetId`, and
|
|
32
|
+
* each Form Field's `datasetFieldId` where the dataset has a field of the
|
|
33
|
+
* same name and a type the submitted value can be stored as. A field the
|
|
34
|
+
* dataset lacks is left unbound, as the Form element's mapping leaves one:
|
|
35
|
+
* its value stays in the Inbox copy, and a form never adds a field to a
|
|
36
|
+
* dataset.
|
|
37
|
+
*
|
|
38
|
+
* The gates are the platform's. The binding is made only where the plan
|
|
39
|
+
* includes the data store (`dataStore`), the data plugin's writer is loaded
|
|
40
|
+
* here and reads the dataset, and the dataset is one this job made or one the
|
|
41
|
+
* site's inventory shows. Records, their allowance and storage are held at
|
|
42
|
+
* each submission by the data plugin's own record target. Anywhere else the
|
|
43
|
+
* form is the form it always was, its submissions in the Inbox.
|
|
44
|
+
*/ /** The unit input a dataset this job made for the form rides in, by id. */ export const AI_FORM_DATASET_MADE_INPUT = 'formDatasetMade';
|
|
45
|
+
/** A field name as two spellings of it compare: `fullName`, `full_name` and "Full name" are one. */ export function aiFormFieldKey(name) {
|
|
46
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().replace(/[^a-z0-9]+/g, '');
|
|
47
|
+
}
|
|
48
|
+
const NUMBER_TYPES = new Set([
|
|
49
|
+
'float',
|
|
50
|
+
'int32',
|
|
51
|
+
'int64',
|
|
52
|
+
'number',
|
|
53
|
+
'integer'
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* Whether a submitted value of this form field type can be stored in a
|
|
57
|
+
* dataset field of this storage type. Every value a form submits is text, so
|
|
58
|
+
* a text field takes any of them and a number field takes a rating; any
|
|
59
|
+
* other pairing is left unbound rather than refused at every submission.
|
|
60
|
+
*/ export function aiFormFieldFitsDataset(fieldType, datasetType) {
|
|
61
|
+
if (datasetType === 'text') return true;
|
|
62
|
+
if (NUMBER_TYPES.has(datasetType)) return fieldType === 'rating';
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Binds a form design to a dataset, in place: the form node's `datasetId`,
|
|
67
|
+
* and `datasetFieldId` on each Form Field the dataset has a field for, by its
|
|
68
|
+
* `fieldName` or its label, each dataset field taken once. The platform's
|
|
69
|
+
* consent field is never bound. A binding the design already carried is
|
|
70
|
+
* replaced, so what the model drew never chooses where a record lands.
|
|
71
|
+
*/ export function aiBindFormToDataset(nodes, formNodeId, dataset) {
|
|
72
|
+
var _form_props;
|
|
73
|
+
const result = {
|
|
74
|
+
bound: [],
|
|
75
|
+
unbound: []
|
|
76
|
+
};
|
|
77
|
+
const form = nodes[formNodeId];
|
|
78
|
+
if ((form == null ? void 0 : form.componentId) !== FORM_COMPONENT_ID) return result;
|
|
79
|
+
const props = _extends({}, (_form_props = form.props) != null ? _form_props : {});
|
|
80
|
+
delete props['datasetName'];
|
|
81
|
+
props['datasetId'] = dataset.id;
|
|
82
|
+
form.props = props;
|
|
83
|
+
const taken = new Set();
|
|
84
|
+
for (const id of collectFormFieldNodeIds(nodes, formNodeId)){
|
|
85
|
+
var _field_props, _fieldProps_fieldName, _fieldProps_label, _fieldProps_fieldType, _matching;
|
|
86
|
+
const field = nodes[id];
|
|
87
|
+
if (!field) continue;
|
|
88
|
+
const fieldProps = _extends({}, (_field_props = field.props) != null ? _field_props : {});
|
|
89
|
+
delete fieldProps['datasetFieldId'];
|
|
90
|
+
field.props = fieldProps;
|
|
91
|
+
const fieldName = String((_fieldProps_fieldName = fieldProps['fieldName']) != null ? _fieldProps_fieldName : '').trim();
|
|
92
|
+
if (!fieldName || isMarketingConsentFieldName(fieldName)) continue;
|
|
93
|
+
const label = String((_fieldProps_label = fieldProps['label']) != null ? _fieldProps_label : '').trim();
|
|
94
|
+
const fieldType = String((_fieldProps_fieldType = fieldProps['fieldType']) != null ? _fieldProps_fieldType : 'text');
|
|
95
|
+
// Its name first, then its label: `fullName` is "Full name" before "Your name" is.
|
|
96
|
+
const matching = (key)=>key ? dataset.fields.find((one)=>!taken.has(one.id) && aiFormFieldKey(one.name) === key && aiFormFieldFitsDataset(fieldType, one.type)) : undefined;
|
|
97
|
+
const match = (_matching = matching(aiFormFieldKey(fieldName))) != null ? _matching : matching(aiFormFieldKey(label));
|
|
98
|
+
if (!match) {
|
|
99
|
+
result.unbound.push(label || fieldName);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
taken.add(match.id);
|
|
103
|
+
fieldProps['datasetFieldId'] = match.id;
|
|
104
|
+
result.bound.push(fieldName);
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
const str = (value)=>typeof value === 'string' ? value.trim() : '';
|
|
109
|
+
/**
|
|
110
|
+
* The dataset a form may write to, or `null` where it may not: the plan
|
|
111
|
+
* includes no data store, the data plugin's writer is not loaded here, the
|
|
112
|
+
* dataset is neither one this job made nor one the site's inventory shows,
|
|
113
|
+
* or it has no field a submission could fill.
|
|
114
|
+
*/ export async function aiReadFormDataset(input, deps = {}) {
|
|
115
|
+
var _ref, _deps_writerFor, _record_facts;
|
|
116
|
+
var _input_inventory;
|
|
117
|
+
const id = str(input.id);
|
|
118
|
+
if (!id) return null;
|
|
119
|
+
if (!checkEntitlement(input.org, 'dataStore')) return null;
|
|
120
|
+
const known = id === str(input.madeId) || ((_ref = (_input_inventory = input.inventory) == null ? void 0 : _input_inventory.datasets) != null ? _ref : []).some((row)=>row.id === id);
|
|
121
|
+
if (!known) return null;
|
|
122
|
+
const keeper = ((_deps_writerFor = deps.writerFor) != null ? _deps_writerFor : pluginResourceDraftWriter)(AI_DATASET_DRAFT_RESOURCE);
|
|
123
|
+
if (!keeper) return null;
|
|
124
|
+
const record = await keeper.writer.read({
|
|
125
|
+
hostId: input.hostId,
|
|
126
|
+
id
|
|
127
|
+
}).catch((error)=>{
|
|
128
|
+
console.error('ai form: the dataset it writes to could not be read', {
|
|
129
|
+
hostId: input.hostId,
|
|
130
|
+
id,
|
|
131
|
+
error
|
|
132
|
+
});
|
|
133
|
+
return null;
|
|
134
|
+
});
|
|
135
|
+
if (!record) return null;
|
|
136
|
+
const facts = (_record_facts = record.facts) != null ? _record_facts : {};
|
|
137
|
+
const address = str(facts.addressField);
|
|
138
|
+
const fields = (Array.isArray(facts.fields) ? facts.fields : []).map((raw)=>raw && typeof raw === 'object' ? raw : {}).map((raw)=>({
|
|
139
|
+
id: str(raw['id']),
|
|
140
|
+
name: str(raw['name']),
|
|
141
|
+
type: str(raw['type']) || 'text'
|
|
142
|
+
})).filter((field)=>field.id && field.name && field.id !== address);
|
|
143
|
+
if (!fields.length) return null;
|
|
144
|
+
return {
|
|
145
|
+
id,
|
|
146
|
+
name: str(record.name) || 'dataset',
|
|
147
|
+
fields
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** What the form's generation is told about the dataset it writes to. */ export function aiFormDatasetPromptLine(dataset) {
|
|
151
|
+
const fields = dataset.fields.map((field)=>`${field.name} (${NUMBER_TYPES.has(field.type) ? 'a number' : 'text'})`);
|
|
152
|
+
return `Each submission is also saved as a record of the dataset “${dataset.name}”, whose fields are: ${fields.join(', ')}. Draw a field for each one a visitor can answer, labeled with the field's name.`;
|
|
153
|
+
}
|
|
154
|
+
/** What the form's row says about where its submissions are also saved. */ export function aiFormDatasetNote(dataset, binding) {
|
|
155
|
+
const kept = `Each submission is also saved as a record of the “${dataset.name}” dataset, in Data.`;
|
|
156
|
+
if (!binding.unbound.length) return kept;
|
|
157
|
+
const one = binding.unbound.length === 1;
|
|
158
|
+
return `${kept} The dataset has no field for ${binding.unbound.map((name)=>`“${name}”`).join(', ')}, so ${one ? 'that answer stays' : 'those answers stay'} in the Inbox only.`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//# sourceMappingURL=ai-job-form-dataset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-form-dataset.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { checkEntitlement } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n collectFormFieldNodeIds,\n FORM_COMPONENT_ID,\n isMarketingConsentFieldName,\n} from '@aglyn/aglyn/app-utils/forms'\nimport { pluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { NodesMap } from '@aglyn/aglyn/types/nodes'\nimport type { AiSiteInventory } from '../model/ai-site-inventory'\nimport { AI_DATASET_DRAFT_RESOURCE } from './ai-build-unit-outcome'\n\n/**\n * A FORM THAT WRITES TO A DATASET (AGL-3616; Zach, 2026-10-10: Aglyn AI's\n * forms can write to datasets — a volunteer sign-up into a Volunteers\n * dataset, an RSVP into an event's attendees, a catering inquiry into\n * Inquiries).\n *\n * The plan says which dataset a form writes to (`writesTo` on its creation);\n * it is never inferred from what a section places. The dataset is built\n * first — a site start's dataset unit, a build's dataset item, or one the\n * site already has — and the form step binds the form it generated the way a\n * person binds one on the Form element: the form node's `datasetId`, and\n * each Form Field's `datasetFieldId` where the dataset has a field of the\n * same name and a type the submitted value can be stored as. A field the\n * dataset lacks is left unbound, as the Form element's mapping leaves one:\n * its value stays in the Inbox copy, and a form never adds a field to a\n * dataset.\n *\n * The gates are the platform's. The binding is made only where the plan\n * includes the data store (`dataStore`), the data plugin's writer is loaded\n * here and reads the dataset, and the dataset is one this job made or one the\n * site's inventory shows. Records, their allowance and storage are held at\n * each submission by the data plugin's own record target. Anywhere else the\n * form is the form it always was, its submissions in the Inbox.\n */\n\n/** The unit input a dataset this job made for the form rides in, by id. */\nexport const AI_FORM_DATASET_MADE_INPUT = 'formDatasetMade'\n\n/** One field of the dataset a form writes to, as its writer reports it: storage type words. */\nexport interface AiFormDatasetField {\n id: string\n name: string\n type: string\n}\n\n/** The dataset a form writes to, with the fields a submission may fill. */\nexport interface AiFormDataset {\n id: string\n name: string\n fields: AiFormDatasetField[]\n}\n\n/** A field name as two spellings of it compare: `fullName`, `full_name` and \"Full name\" are one. */\nexport function aiFormFieldKey(name: string): string {\n return name\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '')\n}\n\nconst NUMBER_TYPES = new Set(['float', 'int32', 'int64', 'number', 'integer'])\n\n/**\n * Whether a submitted value of this form field type can be stored in a\n * dataset field of this storage type. Every value a form submits is text, so\n * a text field takes any of them and a number field takes a rating; any\n * other pairing is left unbound rather than refused at every submission.\n */\nexport function aiFormFieldFitsDataset(fieldType: string, datasetType: string): boolean {\n if (datasetType === 'text') return true\n if (NUMBER_TYPES.has(datasetType)) return fieldType === 'rating'\n return false\n}\n\n/** What a binding came to: the fields bound, and the fields the dataset has no place for. */\nexport interface AiFormDatasetBinding {\n bound: string[]\n unbound: string[]\n}\n\n/**\n * Binds a form design to a dataset, in place: the form node's `datasetId`,\n * and `datasetFieldId` on each Form Field the dataset has a field for, by its\n * `fieldName` or its label, each dataset field taken once. The platform's\n * consent field is never bound. A binding the design already carried is\n * replaced, so what the model drew never chooses where a record lands.\n */\nexport function aiBindFormToDataset(\n nodes: NodesMap,\n formNodeId: string,\n dataset: AiFormDataset,\n): AiFormDatasetBinding {\n const result: AiFormDatasetBinding = { bound: [], unbound: [] }\n const form = nodes[formNodeId]\n if (form?.componentId !== FORM_COMPONENT_ID) return result\n const props = { ...((form.props ?? {}) as Record<string, unknown>) }\n delete props['datasetName']\n props['datasetId'] = dataset.id\n form.props = props as never\n const taken = new Set<string>()\n for (const id of collectFormFieldNodeIds(nodes as never, formNodeId)) {\n const field = nodes[id]\n if (!field) continue\n const fieldProps = { ...((field.props ?? {}) as Record<string, unknown>) }\n delete fieldProps['datasetFieldId']\n field.props = fieldProps as never\n const fieldName = String(fieldProps['fieldName'] ?? '').trim()\n if (!fieldName || isMarketingConsentFieldName(fieldName)) continue\n const label = String(fieldProps['label'] ?? '').trim()\n const fieldType = String(fieldProps['fieldType'] ?? 'text')\n // Its name first, then its label: `fullName` is \"Full name\" before \"Your name\" is.\n const matching = (key: string) =>\n key\n ? dataset.fields.find((one) => !taken.has(one.id) && aiFormFieldKey(one.name) === key && aiFormFieldFitsDataset(fieldType, one.type))\n : undefined\n const match = matching(aiFormFieldKey(fieldName)) ?? matching(aiFormFieldKey(label))\n if (!match) {\n result.unbound.push(label || fieldName)\n continue\n }\n taken.add(match.id)\n fieldProps['datasetFieldId'] = match.id\n result.bound.push(fieldName)\n }\n return result\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\nexport interface AiFormDatasetReadDeps {\n writerFor?: typeof pluginResourceDraftWriter\n}\n\n/**\n * The dataset a form may write to, or `null` where it may not: the plan\n * includes no data store, the data plugin's writer is not loaded here, the\n * dataset is neither one this job made nor one the site's inventory shows,\n * or it has no field a submission could fill.\n */\nexport async function aiReadFormDataset(\n input: {\n hostId: string\n id: string | null | undefined\n org: object | null\n inventory: Pick<AiSiteInventory, 'datasets'> | null\n /** The dataset this job made for the form, by id. */\n madeId?: unknown\n },\n deps: AiFormDatasetReadDeps = {},\n): Promise<AiFormDataset | null> {\n const id = str(input.id)\n if (!id) return null\n if (!checkEntitlement(input.org as never, 'dataStore')) return null\n const known = id === str(input.madeId) || (input.inventory?.datasets ?? []).some((row) => row.id === id)\n if (!known) return null\n const keeper = (deps.writerFor ?? pluginResourceDraftWriter)(AI_DATASET_DRAFT_RESOURCE)\n if (!keeper) return null\n const record = await keeper.writer.read({ hostId: input.hostId, id }).catch((error: unknown) => {\n console.error('ai form: the dataset it writes to could not be read', { hostId: input.hostId, id, error })\n return null\n })\n if (!record) return null\n const facts = (record.facts ?? {}) as { fields?: unknown; addressField?: unknown }\n const address = str(facts.addressField)\n const fields = (Array.isArray(facts.fields) ? (facts.fields as unknown[]) : [])\n .map((raw) => (raw && typeof raw === 'object' ? (raw as Record<string, unknown>) : {}))\n .map((raw) => ({ id: str(raw['id']), name: str(raw['name']), type: str(raw['type']) || 'text' }))\n .filter((field) => field.id && field.name && field.id !== address)\n if (!fields.length) return null\n return { id, name: str(record.name) || 'dataset', fields }\n}\n\n/** What the form's generation is told about the dataset it writes to. */\nexport function aiFormDatasetPromptLine(dataset: AiFormDataset): string {\n const fields = dataset.fields.map((field) => `${field.name} (${NUMBER_TYPES.has(field.type) ? 'a number' : 'text'})`)\n return `Each submission is also saved as a record of the dataset “${dataset.name}”, whose fields are: ${fields.join(', ')}. Draw a field for each one a visitor can answer, labeled with the field's name.`\n}\n\n/** What the form's row says about where its submissions are also saved. */\nexport function aiFormDatasetNote(dataset: AiFormDataset, binding: AiFormDatasetBinding): string {\n const kept = `Each submission is also saved as a record of the “${dataset.name}” dataset, in Data.`\n if (!binding.unbound.length) return kept\n const one = binding.unbound.length === 1\n return `${kept} The dataset has no field for ${binding.unbound.map((name) => `“${name}”`).join(', ')}, so ${\n one ? 'that answer stays' : 'those answers stay'\n } in the Inbox only.`\n}\n"],"names":["checkEntitlement","collectFormFieldNodeIds","FORM_COMPONENT_ID","isMarketingConsentFieldName","pluginResourceDraftWriter","AI_DATASET_DRAFT_RESOURCE","AI_FORM_DATASET_MADE_INPUT","aiFormFieldKey","name","replace","toLowerCase","NUMBER_TYPES","Set","aiFormFieldFitsDataset","fieldType","datasetType","has","aiBindFormToDataset","nodes","formNodeId","dataset","form","result","bound","unbound","componentId","props","id","taken","field","fieldProps","matching","fieldName","String","trim","label","key","fields","find","one","type","undefined","match","push","add","str","value","aiReadFormDataset","input","deps","record","org","known","madeId","inventory","datasets","some","row","keeper","writerFor","writer","read","hostId","catch","error","console","facts","address","addressField","Array","isArray","map","raw","filter","length","aiFormDatasetPromptLine","join","aiFormDatasetNote","binding","kept"],"mappings":"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"}
|
|
@@ -24,6 +24,7 @@ import type { AiDoctrineViolation } from '../runtime/ai-doctrine-validators';
|
|
|
24
24
|
import type { AiSystemBlock, AiTool } from '../runtime/ai-runtime';
|
|
25
25
|
import { readSiteInventory } from '../runtime/site-inventory';
|
|
26
26
|
import { type AiJobAdmission } from './ai-job-admission';
|
|
27
|
+
import { type AiFormDataset, type AiFormDatasetBinding, type AiFormDatasetReadDeps } from './ai-job-form-dataset';
|
|
27
28
|
import type { AiJobStepRunner } from './ai-job-text-step';
|
|
28
29
|
/**
|
|
29
30
|
* The form step (AGL-2913): the generation step of a `form` job, run once a
|
|
@@ -140,18 +141,26 @@ export interface AiFormDraft {
|
|
|
140
141
|
consentFieldName: string | null;
|
|
141
142
|
routing: FormRouting | null;
|
|
142
143
|
answer: AiFormAnswer;
|
|
144
|
+
/** The dataset its submissions are also written to, and what was bound (AGL-3616). */
|
|
145
|
+
dataset?: {
|
|
146
|
+
dataset: AiFormDataset;
|
|
147
|
+
binding: AiFormDatasetBinding;
|
|
148
|
+
};
|
|
143
149
|
}
|
|
144
150
|
/**
|
|
145
151
|
* The draft a validated design and its answer make: the form node bound to
|
|
146
|
-
* `formId` and captioned `name`,
|
|
147
|
-
*
|
|
148
|
-
*
|
|
152
|
+
* `formId` and captioned `name`, the platform's consent field when the
|
|
153
|
+
* fields can yield an email address, the canvas root above, and the
|
|
154
|
+
* declaration read off the result. A dataset binding is made only where the
|
|
155
|
+
* plan said the form writes to one this step may bind (`identity.dataset`,
|
|
156
|
+
* AGL-3616); any the model drew is dropped. Pure; the input tree is not
|
|
149
157
|
* changed.
|
|
150
158
|
*/
|
|
151
159
|
export declare function aiFormDraft(tree: Pick<AiValidatedTree, 'rootId' | 'nodes'>, answer: Record<string, unknown>, identity: {
|
|
152
160
|
formId: string;
|
|
153
161
|
name: string;
|
|
154
162
|
decisions?: AiFormDecisions;
|
|
163
|
+
dataset?: AiFormDataset | null;
|
|
155
164
|
}): AiFormDraft;
|
|
156
165
|
/**
|
|
157
166
|
* What a draft would break once published, as building-rule violations
|
|
@@ -170,6 +179,7 @@ export declare function aiFormDraftCheck(identity: {
|
|
|
170
179
|
formId: string;
|
|
171
180
|
name: string;
|
|
172
181
|
decisions?: AiFormDecisions;
|
|
182
|
+
dataset?: AiFormDataset | null;
|
|
173
183
|
}): {
|
|
174
184
|
extend: (tree: AiValidatedTree, answer: Record<string, unknown>) => AiDoctrineViolation[];
|
|
175
185
|
draftFor: (tree: AiValidatedTree) => AiFormDraft | undefined;
|
|
@@ -280,6 +290,8 @@ export interface AiJobFormStepDeps {
|
|
|
280
290
|
readInventory?: typeof readSiteInventory;
|
|
281
291
|
/** The platform's duplicate module, for a plan that starts from a copy. */
|
|
282
292
|
duplicate?: typeof duplicateResource;
|
|
293
|
+
/** The data plugin's writer lookup the form's dataset is read through (AGL-3616); specs hand in a fake. */
|
|
294
|
+
datasetWriterFor?: AiFormDatasetReadDeps['writerFor'];
|
|
283
295
|
}
|
|
284
296
|
export declare function createAiJobFormStep(deps?: AiJobFormStepDeps): AiJobStepRunner;
|
|
285
297
|
export declare const runAiJobFormStep: AiJobStepRunner;
|
|
@@ -28,6 +28,7 @@ import { aiDoctrineTreeTool, runValidatedGeneration } from "../runtime/ai-doctri
|
|
|
28
28
|
import { AI_PALETTE } from "../runtime/ai-palette.generated.js";
|
|
29
29
|
import { readSiteInventory } from "../runtime/site-inventory.js";
|
|
30
30
|
import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
31
|
+
import { AI_FORM_DATASET_MADE_INPUT, aiBindFormToDataset, aiFormDatasetNote, aiFormDatasetPromptLine, aiReadFormDataset } from "./ai-job-form-dataset.js";
|
|
31
32
|
import { aiJobPlanCreationsRefusal } from "./ai-job-plan-creations.js";
|
|
32
33
|
import { aiJobDraftId } from "./ai-job-draft-ids.js";
|
|
33
34
|
import { aiDraftAdmissionRefusal, aiDraftAllowanceRefusal, aiSiteSubdomain, readAiDraft, writeAiDraft } from "./ai-job-drafts.js";
|
|
@@ -209,9 +210,11 @@ function oneLine(value, max) {
|
|
|
209
210
|
}
|
|
210
211
|
/**
|
|
211
212
|
* The draft a validated design and its answer make: the form node bound to
|
|
212
|
-
* `formId` and captioned `name`,
|
|
213
|
-
*
|
|
214
|
-
*
|
|
213
|
+
* `formId` and captioned `name`, the platform's consent field when the
|
|
214
|
+
* fields can yield an email address, the canvas root above, and the
|
|
215
|
+
* declaration read off the result. A dataset binding is made only where the
|
|
216
|
+
* plan said the form writes to one this step may bind (`identity.dataset`,
|
|
217
|
+
* AGL-3616); any the model drew is dropped. Pure; the input tree is not
|
|
215
218
|
* changed.
|
|
216
219
|
*/ export function aiFormDraft(tree, answer, identity) {
|
|
217
220
|
var _identity_decisions;
|
|
@@ -233,8 +236,8 @@ function oneLine(value, max) {
|
|
|
233
236
|
let consentFieldName = null;
|
|
234
237
|
if ((form == null ? void 0 : form.componentId) === FORM_COMPONENT_ID) {
|
|
235
238
|
const props = form.props;
|
|
236
|
-
// A dataset binding decides where every submission is also written
|
|
237
|
-
//
|
|
239
|
+
// A dataset binding decides where every submission is also written: the
|
|
240
|
+
// plan's to say (AGL-3616), bound below, never the model's to draw.
|
|
238
241
|
delete props['datasetId'];
|
|
239
242
|
delete props['datasetName'];
|
|
240
243
|
props[FORM_ID_PROP] = identity.formId;
|
|
@@ -269,6 +272,12 @@ function oneLine(value, max) {
|
|
|
269
272
|
}
|
|
270
273
|
form.parentId = CANVAS_ROOT_ELEMENT_ID;
|
|
271
274
|
}
|
|
275
|
+
// The dataset the plan says the form writes to (AGL-3616), bound as the
|
|
276
|
+
// Form element binds one.
|
|
277
|
+
const dataset = identity.dataset && (form == null ? void 0 : form.componentId) === FORM_COMPONENT_ID ? {
|
|
278
|
+
dataset: identity.dataset,
|
|
279
|
+
binding: aiBindFormToDataset(nodes, formNodeId, identity.dataset)
|
|
280
|
+
} : undefined;
|
|
272
281
|
nodes[CANVAS_ROOT_ELEMENT_ID] = {
|
|
273
282
|
$id: CANVAS_ROOT_ELEMENT_ID,
|
|
274
283
|
componentId: 'div',
|
|
@@ -276,7 +285,7 @@ function oneLine(value, max) {
|
|
|
276
285
|
formNodeId
|
|
277
286
|
]
|
|
278
287
|
};
|
|
279
|
-
return {
|
|
288
|
+
return _extends({
|
|
280
289
|
nodes,
|
|
281
290
|
rootId: CANVAS_ROOT_ELEMENT_ID,
|
|
282
291
|
formNodeId,
|
|
@@ -286,7 +295,9 @@ function oneLine(value, max) {
|
|
|
286
295
|
lead: true
|
|
287
296
|
} : null,
|
|
288
297
|
answer: parsed
|
|
289
|
-
}
|
|
298
|
+
}, dataset ? {
|
|
299
|
+
dataset
|
|
300
|
+
} : {});
|
|
290
301
|
}
|
|
291
302
|
/**
|
|
292
303
|
* What a draft would break once published, as building-rule violations
|
|
@@ -536,6 +547,7 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
536
547
|
const duplicate = (_deps_duplicate = deps.duplicate) != null ? _deps_duplicate : duplicateResource;
|
|
537
548
|
return async ({ job, now, signal, firestore, modelFor })=>{
|
|
538
549
|
var _ref, _orgSnapshot_data;
|
|
550
|
+
var _job_inputs;
|
|
539
551
|
const hostId = job.hostId;
|
|
540
552
|
if (!hostId) throw new Error('a form job names no site, and its admission refuses one');
|
|
541
553
|
const output = (draft, extra = {})=>_extends({
|
|
@@ -662,10 +674,22 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
662
674
|
submissions: aiSiteSubmissions(job.inputs)
|
|
663
675
|
};
|
|
664
676
|
const { audience } = aiSiteWords(job.inputs);
|
|
677
|
+
// The dataset the plan says the form writes to (AGL-3616), where this
|
|
678
|
+
// workspace may bind one; otherwise the form is the form it always was.
|
|
679
|
+
const dataset = await aiReadFormDataset({
|
|
680
|
+
hostId,
|
|
681
|
+
id: creation == null ? void 0 : creation.writesTo,
|
|
682
|
+
org,
|
|
683
|
+
inventory,
|
|
684
|
+
madeId: (_job_inputs = job.inputs) == null ? void 0 : _job_inputs[AI_FORM_DATASET_MADE_INPUT]
|
|
685
|
+
}, deps.datasetWriterFor ? {
|
|
686
|
+
writerFor: deps.datasetWriterFor
|
|
687
|
+
} : {});
|
|
665
688
|
const check = aiFormDraftCheck({
|
|
666
689
|
formId: draftId,
|
|
667
690
|
name,
|
|
668
|
-
decisions
|
|
691
|
+
decisions,
|
|
692
|
+
dataset
|
|
669
693
|
});
|
|
670
694
|
const result = await runValidatedGeneration('form', _extends({
|
|
671
695
|
step: 'job.form',
|
|
@@ -680,7 +704,10 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
680
704
|
audience,
|
|
681
705
|
submissions: decisions.submissions
|
|
682
706
|
}),
|
|
683
|
-
...sourceLines
|
|
707
|
+
...sourceLines,
|
|
708
|
+
...dataset ? [
|
|
709
|
+
aiFormDatasetPromptLine(dataset)
|
|
710
|
+
] : []
|
|
684
711
|
].join('\n')
|
|
685
712
|
}
|
|
686
713
|
],
|
|
@@ -728,6 +755,7 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
728
755
|
// What the person decides next, then the facts the brief did not give (AGL-3056).
|
|
729
756
|
const note = [
|
|
730
757
|
aiFormOutputNote(draft.answer),
|
|
758
|
+
draft.dataset ? aiFormDatasetNote(draft.dataset.dataset, draft.dataset.binding) : null,
|
|
731
759
|
aiBracketedFactsNote({
|
|
732
760
|
tree: {
|
|
733
761
|
rootId: result.value.rootId,
|