@aglyn/plugins-ai 1.0.0-beta.172 → 1.0.0-beta.174
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/jobs/ai-email-bindings.d.ts +11 -1
- package/src/lib/jobs/ai-email-bindings.js +12 -2
- package/src/lib/jobs/ai-email-bindings.js.map +1 -1
- package/src/lib/jobs/ai-job-campaign-step.js +1 -0
- package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.js +9 -3
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +53 -8
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-ai",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.174",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,22 +25,22 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-enums": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-data-types": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
37
|
-
"@aglyn/shared-util-errors": "1.0.0-beta.
|
|
38
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
39
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
40
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
41
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
42
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
43
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.174",
|
|
29
|
+
"@aglyn/shared-data-enums": "1.0.0-beta.174",
|
|
30
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.174",
|
|
31
|
+
"@aglyn/shared-data-types": "1.0.0-beta.174",
|
|
32
|
+
"@aglyn/shared-ui-email-campaigns": "1.0.0-beta.174",
|
|
33
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.174",
|
|
34
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.174",
|
|
35
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.174",
|
|
36
|
+
"@aglyn/shared-util-email": "1.0.0-beta.174",
|
|
37
|
+
"@aglyn/shared-util-errors": "1.0.0-beta.174",
|
|
38
|
+
"@aglyn/shared-util-http": "1.0.0-beta.174",
|
|
39
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.174",
|
|
40
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.174",
|
|
41
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.174",
|
|
42
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.174",
|
|
43
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.174",
|
|
44
44
|
"@swc/helpers": "0.5.23"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
@@ -85,8 +85,18 @@ export declare function suggestAiCampaignList(input: {
|
|
|
85
85
|
hostId: string;
|
|
86
86
|
brief: string;
|
|
87
87
|
}): Promise<AiNamedRecord | null>;
|
|
88
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The send time a list's past sends on this site suggest, or `null` with too
|
|
90
|
+
* little history.
|
|
91
|
+
*
|
|
92
|
+
* Sends are the organization's (`orgs/{orgId}/campaigns`), and a list is too,
|
|
93
|
+
* so the org's sends to the list are narrowed to the ones sent AS this site
|
|
94
|
+
* (`hostId`): another site's audience opening at another hour is not this
|
|
95
|
+
* site's history. Three equality filters, which Firestore serves by merging
|
|
96
|
+
* single-field indexes — no composite index.
|
|
97
|
+
*/
|
|
89
98
|
export declare function readAiListSendTime(firestore: Firestore, input: {
|
|
99
|
+
orgId: string;
|
|
90
100
|
hostId: string;
|
|
91
101
|
listId: string;
|
|
92
102
|
}): Promise<CampaignSendTimeSuggestion | null>;
|
|
@@ -111,8 +111,18 @@ function millisOf(value) {
|
|
|
111
111
|
const toMillis = value == null ? void 0 : value.toMillis;
|
|
112
112
|
return typeof toMillis === 'function' ? toMillis.call(value) : 0;
|
|
113
113
|
}
|
|
114
|
-
/**
|
|
115
|
-
|
|
114
|
+
/**
|
|
115
|
+
* The send time a list's past sends on this site suggest, or `null` with too
|
|
116
|
+
* little history.
|
|
117
|
+
*
|
|
118
|
+
* Sends are the organization's (`orgs/{orgId}/campaigns`), and a list is too,
|
|
119
|
+
* so the org's sends to the list are narrowed to the ones sent AS this site
|
|
120
|
+
* (`hostId`): another site's audience opening at another hour is not this
|
|
121
|
+
* site's history. Three equality filters, which Firestore serves by merging
|
|
122
|
+
* single-field indexes — no composite index.
|
|
123
|
+
*/ export async function readAiListSendTime(firestore, input) {
|
|
124
|
+
if (!input.orgId || !input.hostId) return null;
|
|
125
|
+
const snapshot = await firestore.collection('orgs').doc(input.orgId).collection('campaigns').where('hostId', '==', input.hostId).where('listId', '==', input.listId).where('status', '==', 'sent').select('sentAt', 'stats.delivered', 'stats.uniqueOpens').limit(AI_CAMPAIGN_SEND_HISTORY_SCAN).get();
|
|
116
126
|
return suggestCampaignSendTime(snapshot.docs.map((doc)=>{
|
|
117
127
|
var _doc_get, _doc_get1;
|
|
118
128
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-email-bindings.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 {\n campaignSendTimeLabel,\n suggestCampaignSendTime,\n type CampaignSendTimeSuggestion,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-send-time'\nimport { orgDataCollectionForHost } from '@aglyn/tenant-data-admin/server/organizations'\n\n/**\n * What an email or campaign job binds and suggests WITHOUT the model\n * (AGL-2912).\n *\n * What AI generation is published to send is the brief, a summary of the\n * site and the content being worked on — not email lists, contacts,\n * recipients, CRM records, product records or engagement statistics. So\n * everything here that touches one of those runs in code, on the server, and\n * hands the model at most a count:\n *\n * - PRODUCTS are bound by id: the ones the member picked for the job, else\n * the ones the brief names by their exact name. The model learns only how\n * many product blocks to place; the ids are set on those blocks after it\n * answers, and the send fills each block's name, price and picture.\n * - THE LIST a campaign is for is suggested when the brief names one of the\n * org's lists by name, and is set on nothing: the drafted email is aimed at\n * nobody until a person chooses.\n * - THE SEND TIME is the shared rule over that list's past sends on this\n * site — when each went out, how many were delivered and how many people\n * opened it — and is said only in the output's note.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The most products one email binds. */\nexport const AI_EMAIL_MAX_PRODUCTS = 6\n\n/** How many of a site's products are read for the names a brief might use. */\nexport const AI_EMAIL_PRODUCT_SCAN = 200\n\n/** How many of an org's lists are read: the composer's list picker window. */\nexport const AI_CAMPAIGN_LIST_SCAN = 50\n\n/** How many of a list's past sends on the site the send time is taken from. */\nexport const AI_CAMPAIGN_SEND_HISTORY_SCAN = 50\n\n/** A record a brief may name, by its id and its name. */\nexport interface AiNamedRecord {\n id: string\n name: string\n}\n\n/** Lowercase words joined by single spaces, for matching a name inside text. */\nfunction wordsOf(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, ' ')\n .trim()\n}\n\n/**\n * The records a text names by their whole name, in the order it names them.\n * A name must stand as its own words (\"Rye loaf\" is not named by \"rye\n * loaves\"), a longer name takes the words it shares with a shorter one (\"Rye\n * loaf\" over \"Rye\"), and a name shorter than three characters names nothing.\n */\nexport function aiRecordsNamedIn(\n text: string,\n records: readonly AiNamedRecord[],\n max: number,\n): AiNamedRecord[] {\n const haystack = ` ${wordsOf(text)} `\n const hits: Array<{ record: AiNamedRecord; at: number; end: number }> = []\n for (const record of records) {\n const needle = wordsOf(record.name)\n if (needle.length < 3) continue\n const at = haystack.indexOf(` ${needle} `)\n if (at !== -1) hits.push({ record, at, end: at + needle.length + 1 })\n }\n const kept: typeof hits = []\n for (const hit of [...hits].sort((a, b) => b.end - b.at - (a.end - a.at))) {\n if (kept.some((other) => hit.at < other.end && hit.end > other.at)) continue\n kept.push(hit)\n }\n return kept\n .sort((a, b) => a.at - b.at)\n .map((hit) => hit.record)\n .slice(0, Math.max(0, max))\n}\n\n/**\n * The product ids a member picked for the job, as the create door stores a\n * scalar input: one string of ids separated by commas or spaces.\n */\nexport function aiEmailPickedProductIds(inputs: Readonly<Record<string, unknown>> | undefined): string[] {\n const raw = inputs?.['productIds']\n if (typeof raw !== 'string') return []\n const ids = raw.split(/[\\s,]+/).filter((id) => /^[A-Za-z0-9_-]{1,128}$/.test(id))\n return [...new Set(ids)].slice(0, AI_EMAIL_MAX_PRODUCTS)\n}\n\nexport interface AiEmailProductBinding {\n /** The product ids bound, in the order the email's product blocks take them. */\n ids: string[]\n /** How many the member picked; 0 when the ids came from the brief. */\n picked: number\n}\n\n/**\n * The products an email binds: the member's picks that still exist, else the\n * ones the brief names. Reads ids and names, and returns only ids.\n */\nexport async function resolveAiEmailProducts(\n firestore: Firestore,\n input: { hostId: string; brief: string; inputs: Readonly<Record<string, unknown>> | undefined },\n): Promise<AiEmailProductBinding> {\n const products = firestore.collection('hosts').doc(input.hostId).collection('products')\n const picked = aiEmailPickedProductIds(input.inputs)\n if (picked.length) {\n const snapshots = await Promise.all(picked.map((id) => products.doc(id).get()))\n return {\n ids: snapshots\n .filter((snapshot) => snapshot.exists && snapshot.get('deletedAt') == null)\n .map((snapshot) => snapshot.id),\n picked: picked.length,\n }\n }\n const snapshot = await products.select('name', 'deletedAt').limit(AI_EMAIL_PRODUCT_SCAN).get()\n const rows = snapshot.docs\n .filter((doc) => doc.get('deletedAt') == null)\n .map((doc) => ({ id: doc.id, name: String(doc.get('name') ?? '') }))\n return {\n ids: aiRecordsNamedIn(input.brief, rows, AI_EMAIL_MAX_PRODUCTS).map((row) => row.id),\n picked: 0,\n }\n}\n\n/**\n * The org list a campaign's brief names, found the way the send path finds an\n * org's lists; `null` when it names none.\n */\nexport async function suggestAiCampaignList(input: {\n hostId: string\n brief: string\n}): Promise<AiNamedRecord | null> {\n const contacts = await orgDataCollectionForHost(input.hostId, 'contacts')\n const lists = contacts.parent?.collection('lists')\n if (!lists) return null\n const snapshot = await lists.select('name').limit(AI_CAMPAIGN_LIST_SCAN).get()\n const rows = snapshot.docs.map((doc) => ({ id: doc.id, name: String(doc.get('name') ?? '') }))\n return aiRecordsNamedIn(input.brief, rows, 1)[0] ?? null\n}\n\nfunction millisOf(value: unknown): number {\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'number') return value\n const toMillis = (value as { toMillis?: () => number } | null)?.toMillis\n return typeof toMillis === 'function' ? toMillis.call(value) : 0\n}\n\n/** The send time a list's past sends on this site suggest, or `null` with too little history. */\nexport async function readAiListSendTime(\n firestore: Firestore,\n input: { hostId: string; listId: string },\n): Promise<CampaignSendTimeSuggestion | null> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(input.hostId)\n .collection('campaigns')\n .where('listId', '==', input.listId)\n .where('status', '==', 'sent')\n .select('sentAt', 'stats.delivered', 'stats.uniqueOpens')\n .limit(AI_CAMPAIGN_SEND_HISTORY_SCAN)\n .get()\n return suggestCampaignSendTime(\n snapshot.docs.map((doc) => ({\n sentAtMs: millisOf(doc.get('sentAt')),\n delivered: Number(doc.get('stats.delivered') ?? 0),\n uniqueOpens: Number(doc.get('stats.uniqueOpens') ?? 0),\n })),\n )\n}\n\n/** What an email's output says about its products; `null` when it binds none and none were picked. */\nexport function aiEmailProductNote(binding: AiEmailProductBinding): string | null {\n const bound = binding.ids.length\n const missing = binding.picked - bound\n const parts: string[] = []\n if (bound === 1) {\n parts.push('Its product card shows the product’s current name, price and picture when it is sent.')\n } else if (bound > 1) {\n parts.push(`Its ${bound} product cards show each product’s current name, price and picture when it is sent.`)\n }\n if (missing > 0) {\n parts.push(\n `${missing} of the products picked no longer ${missing === 1 ? 'exists' : 'exist'}, so the email places ${\n bound || 'none'\n }.`,\n )\n }\n return parts.length ? parts.join(' ') : null\n}\n\n/** What a campaign's output says about who it is for and when to send it. */\nexport function aiCampaignAudienceNote(\n list: AiNamedRecord | null,\n sendTime: CampaignSendTimeSuggestion | null,\n): string {\n if (!list) {\n return 'The email is aimed at nobody yet: choose who receives it before you schedule it.'\n }\n const when = sendTime\n ? ` Past campaigns to that list on this site were opened most when sent on ${campaignSendTimeLabel(\n sendTime,\n )}, across ${sendTime.measured} sends.`\n : ' That list has too little send history on this site to suggest a send time.'\n return `The brief names the list \"${list.name}\". The email is aimed at nobody yet: choose \"${list.name}\" as its audience before you schedule it.${when}`\n}\n"],"names":["campaignSendTimeLabel","suggestCampaignSendTime","orgDataCollectionForHost","AI_EMAIL_MAX_PRODUCTS","AI_EMAIL_PRODUCT_SCAN","AI_CAMPAIGN_LIST_SCAN","AI_CAMPAIGN_SEND_HISTORY_SCAN","wordsOf","value","toLowerCase","replace","trim","aiRecordsNamedIn","text","records","max","haystack","hits","record","needle","name","length","at","indexOf","push","end","kept","hit","sort","a","b","some","other","map","slice","Math","aiEmailPickedProductIds","inputs","raw","ids","split","filter","id","test","Set","resolveAiEmailProducts","firestore","input","products","collection","doc","hostId","picked","snapshots","Promise","all","get","snapshot","exists","select","limit","rows","docs","String","brief","row","suggestAiCampaignList","contacts","lists","parent","millisOf","Date","getTime","toMillis","call","readAiListSendTime","where","listId","sentAtMs","delivered","Number","uniqueOpens","aiEmailProductNote","binding","bound","missing","parts","join","aiCampaignAudienceNote","list","sendTime","when","measured"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-email-bindings.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 {\n campaignSendTimeLabel,\n suggestCampaignSendTime,\n type CampaignSendTimeSuggestion,\n} from '@aglyn/shared-ui-email-campaigns/model/campaign-send-time'\nimport { orgDataCollectionForHost } from '@aglyn/tenant-data-admin/server/organizations'\n\n/**\n * What an email or campaign job binds and suggests WITHOUT the model\n * (AGL-2912).\n *\n * What AI generation is published to send is the brief, a summary of the\n * site and the content being worked on — not email lists, contacts,\n * recipients, CRM records, product records or engagement statistics. So\n * everything here that touches one of those runs in code, on the server, and\n * hands the model at most a count:\n *\n * - PRODUCTS are bound by id: the ones the member picked for the job, else\n * the ones the brief names by their exact name. The model learns only how\n * many product blocks to place; the ids are set on those blocks after it\n * answers, and the send fills each block's name, price and picture.\n * - THE LIST a campaign is for is suggested when the brief names one of the\n * org's lists by name, and is set on nothing: the drafted email is aimed at\n * nobody until a person chooses.\n * - THE SEND TIME is the shared rule over that list's past sends on this\n * site — when each went out, how many were delivered and how many people\n * opened it — and is said only in the output's note.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The most products one email binds. */\nexport const AI_EMAIL_MAX_PRODUCTS = 6\n\n/** How many of a site's products are read for the names a brief might use. */\nexport const AI_EMAIL_PRODUCT_SCAN = 200\n\n/** How many of an org's lists are read: the composer's list picker window. */\nexport const AI_CAMPAIGN_LIST_SCAN = 50\n\n/** How many of a list's past sends on the site the send time is taken from. */\nexport const AI_CAMPAIGN_SEND_HISTORY_SCAN = 50\n\n/** A record a brief may name, by its id and its name. */\nexport interface AiNamedRecord {\n id: string\n name: string\n}\n\n/** Lowercase words joined by single spaces, for matching a name inside text. */\nfunction wordsOf(value: string): string {\n return value\n .toLowerCase()\n .replace(/[^\\p{L}\\p{N}]+/gu, ' ')\n .trim()\n}\n\n/**\n * The records a text names by their whole name, in the order it names them.\n * A name must stand as its own words (\"Rye loaf\" is not named by \"rye\n * loaves\"), a longer name takes the words it shares with a shorter one (\"Rye\n * loaf\" over \"Rye\"), and a name shorter than three characters names nothing.\n */\nexport function aiRecordsNamedIn(\n text: string,\n records: readonly AiNamedRecord[],\n max: number,\n): AiNamedRecord[] {\n const haystack = ` ${wordsOf(text)} `\n const hits: Array<{ record: AiNamedRecord; at: number; end: number }> = []\n for (const record of records) {\n const needle = wordsOf(record.name)\n if (needle.length < 3) continue\n const at = haystack.indexOf(` ${needle} `)\n if (at !== -1) hits.push({ record, at, end: at + needle.length + 1 })\n }\n const kept: typeof hits = []\n for (const hit of [...hits].sort((a, b) => b.end - b.at - (a.end - a.at))) {\n if (kept.some((other) => hit.at < other.end && hit.end > other.at)) continue\n kept.push(hit)\n }\n return kept\n .sort((a, b) => a.at - b.at)\n .map((hit) => hit.record)\n .slice(0, Math.max(0, max))\n}\n\n/**\n * The product ids a member picked for the job, as the create door stores a\n * scalar input: one string of ids separated by commas or spaces.\n */\nexport function aiEmailPickedProductIds(inputs: Readonly<Record<string, unknown>> | undefined): string[] {\n const raw = inputs?.['productIds']\n if (typeof raw !== 'string') return []\n const ids = raw.split(/[\\s,]+/).filter((id) => /^[A-Za-z0-9_-]{1,128}$/.test(id))\n return [...new Set(ids)].slice(0, AI_EMAIL_MAX_PRODUCTS)\n}\n\nexport interface AiEmailProductBinding {\n /** The product ids bound, in the order the email's product blocks take them. */\n ids: string[]\n /** How many the member picked; 0 when the ids came from the brief. */\n picked: number\n}\n\n/**\n * The products an email binds: the member's picks that still exist, else the\n * ones the brief names. Reads ids and names, and returns only ids.\n */\nexport async function resolveAiEmailProducts(\n firestore: Firestore,\n input: { hostId: string; brief: string; inputs: Readonly<Record<string, unknown>> | undefined },\n): Promise<AiEmailProductBinding> {\n const products = firestore.collection('hosts').doc(input.hostId).collection('products')\n const picked = aiEmailPickedProductIds(input.inputs)\n if (picked.length) {\n const snapshots = await Promise.all(picked.map((id) => products.doc(id).get()))\n return {\n ids: snapshots\n .filter((snapshot) => snapshot.exists && snapshot.get('deletedAt') == null)\n .map((snapshot) => snapshot.id),\n picked: picked.length,\n }\n }\n const snapshot = await products.select('name', 'deletedAt').limit(AI_EMAIL_PRODUCT_SCAN).get()\n const rows = snapshot.docs\n .filter((doc) => doc.get('deletedAt') == null)\n .map((doc) => ({ id: doc.id, name: String(doc.get('name') ?? '') }))\n return {\n ids: aiRecordsNamedIn(input.brief, rows, AI_EMAIL_MAX_PRODUCTS).map((row) => row.id),\n picked: 0,\n }\n}\n\n/**\n * The org list a campaign's brief names, found the way the send path finds an\n * org's lists; `null` when it names none.\n */\nexport async function suggestAiCampaignList(input: {\n hostId: string\n brief: string\n}): Promise<AiNamedRecord | null> {\n const contacts = await orgDataCollectionForHost(input.hostId, 'contacts')\n const lists = contacts.parent?.collection('lists')\n if (!lists) return null\n const snapshot = await lists.select('name').limit(AI_CAMPAIGN_LIST_SCAN).get()\n const rows = snapshot.docs.map((doc) => ({ id: doc.id, name: String(doc.get('name') ?? '') }))\n return aiRecordsNamedIn(input.brief, rows, 1)[0] ?? null\n}\n\nfunction millisOf(value: unknown): number {\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'number') return value\n const toMillis = (value as { toMillis?: () => number } | null)?.toMillis\n return typeof toMillis === 'function' ? toMillis.call(value) : 0\n}\n\n/**\n * The send time a list's past sends on this site suggest, or `null` with too\n * little history.\n *\n * Sends are the organization's (`orgs/{orgId}/campaigns`), and a list is too,\n * so the org's sends to the list are narrowed to the ones sent AS this site\n * (`hostId`): another site's audience opening at another hour is not this\n * site's history. Three equality filters, which Firestore serves by merging\n * single-field indexes — no composite index.\n */\nexport async function readAiListSendTime(\n firestore: Firestore,\n input: { orgId: string; hostId: string; listId: string },\n): Promise<CampaignSendTimeSuggestion | null> {\n if (!input.orgId || !input.hostId) return null\n const snapshot = await firestore\n .collection('orgs')\n .doc(input.orgId)\n .collection('campaigns')\n .where('hostId', '==', input.hostId)\n .where('listId', '==', input.listId)\n .where('status', '==', 'sent')\n .select('sentAt', 'stats.delivered', 'stats.uniqueOpens')\n .limit(AI_CAMPAIGN_SEND_HISTORY_SCAN)\n .get()\n return suggestCampaignSendTime(\n snapshot.docs.map((doc) => ({\n sentAtMs: millisOf(doc.get('sentAt')),\n delivered: Number(doc.get('stats.delivered') ?? 0),\n uniqueOpens: Number(doc.get('stats.uniqueOpens') ?? 0),\n })),\n )\n}\n\n/** What an email's output says about its products; `null` when it binds none and none were picked. */\nexport function aiEmailProductNote(binding: AiEmailProductBinding): string | null {\n const bound = binding.ids.length\n const missing = binding.picked - bound\n const parts: string[] = []\n if (bound === 1) {\n parts.push('Its product card shows the product’s current name, price and picture when it is sent.')\n } else if (bound > 1) {\n parts.push(`Its ${bound} product cards show each product’s current name, price and picture when it is sent.`)\n }\n if (missing > 0) {\n parts.push(\n `${missing} of the products picked no longer ${missing === 1 ? 'exists' : 'exist'}, so the email places ${\n bound || 'none'\n }.`,\n )\n }\n return parts.length ? parts.join(' ') : null\n}\n\n/** What a campaign's output says about who it is for and when to send it. */\nexport function aiCampaignAudienceNote(\n list: AiNamedRecord | null,\n sendTime: CampaignSendTimeSuggestion | null,\n): string {\n if (!list) {\n return 'The email is aimed at nobody yet: choose who receives it before you schedule it.'\n }\n const when = sendTime\n ? ` Past campaigns to that list on this site were opened most when sent on ${campaignSendTimeLabel(\n sendTime,\n )}, across ${sendTime.measured} sends.`\n : ' That list has too little send history on this site to suggest a send time.'\n return `The brief names the list \"${list.name}\". The email is aimed at nobody yet: choose \"${list.name}\" as its audience before you schedule it.${when}`\n}\n"],"names":["campaignSendTimeLabel","suggestCampaignSendTime","orgDataCollectionForHost","AI_EMAIL_MAX_PRODUCTS","AI_EMAIL_PRODUCT_SCAN","AI_CAMPAIGN_LIST_SCAN","AI_CAMPAIGN_SEND_HISTORY_SCAN","wordsOf","value","toLowerCase","replace","trim","aiRecordsNamedIn","text","records","max","haystack","hits","record","needle","name","length","at","indexOf","push","end","kept","hit","sort","a","b","some","other","map","slice","Math","aiEmailPickedProductIds","inputs","raw","ids","split","filter","id","test","Set","resolveAiEmailProducts","firestore","input","products","collection","doc","hostId","picked","snapshots","Promise","all","get","snapshot","exists","select","limit","rows","docs","String","brief","row","suggestAiCampaignList","contacts","lists","parent","millisOf","Date","getTime","toMillis","call","readAiListSendTime","orgId","where","listId","sentAtMs","delivered","Number","uniqueOpens","aiEmailProductNote","binding","bound","missing","parts","join","aiCampaignAudienceNote","list","sendTime","when","measured"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-campaign-step.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 { checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n PluginDraftRecord,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { aiModelForStep } from '../providers/routing'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n aiCampaignAudienceNote,\n readAiListSendTime,\n resolveAiEmailProducts,\n suggestAiCampaignList,\n} from './ai-email-bindings'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiSiteSubdomain } from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiLimitReview,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport {\n AI_EMAIL_DESIGN_RESOURCE,\n AI_EMAIL_PLUGIN_ID,\n AI_EMAIL_SAVE_FAILURE_COPY,\n AI_JOB_EMAIL_STEP_BUDGETS,\n aiEmailDesignContent,\n aiEmailDesignOutput,\n aiJobInputName,\n generateAiEmail,\n type AiEmailCopy,\n} from './ai-job-email-step'\nimport {\n aiPluginDraftAdmissionRefusal,\n aiPluginDraftWriter,\n type AiPluginDraftWriterLookup,\n} from './ai-job-plugin-drafts'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The campaign step (AGL-2912): one brief becomes an email design AND the\n * draft campaign that would send it.\n *\n * A campaign is an unplanned kind — there is one thing to build and a plan\n * over it would be a plan of one — so the job runs this step alone, and the\n * design it writes is the email step's, generated through `generateAiEmail`\n * so both kinds obey the same doctrine and the same email rules.\n *\n * ## Nothing is sent, and nothing is aimed\n *\n * The campaign is written `draft`, with no audience, no schedule and no\n * sender: a draft campaign is not a queued one, the scheduled processor\n * queries `status == 'scheduled'`, and the send route is the only thing that\n * mails a campaign. Choosing WHO receives it stays a person's decision, so\n * the model is never shown the org's lists, its contacts or what past sends\n * did. Code reads the list the brief names and the send time that list's own\n * history suggests, AFTER the answer, and both reach the person as the\n * output's `note` — never the prompt.\n *\n * ## Where campaign email begins\n *\n * A site whose plan sends no campaign email can still generate email designs;\n * the campaign is the part that needs somewhere to send. So the step gates on\n * the same entitlement the composer and the send route read, and stops the\n * job for a person rather than failing it. The refusal names no plan, because\n * the billing page is the one place that does.\n *\n * ## Run again and it finds its drafts\n *\n * Each write is keyed by the id the job recorded for its draft when it was\n * created, so a run cut off between them finds what it wrote: two drafts are\n * reported, a design alone is drafted into a campaign from the copy the\n * design already stores, and neither spends.\n */\n\n/** The id the marketing plugin is registered under. */\nexport const AI_CAMPAIGN_PLUGIN_ID = 'marketing'\n\n/** The resource the marketing plugin writes a draft campaign under. */\nexport const AI_CAMPAIGN_RESOURCE = 'campaign'\n\n/** A campaign's name when neither the job nor its subject lines give one. */\nexport const AI_JOB_CAMPAIGN_DEFAULT_NAME = 'New campaign'\n\n/** A job's failure when the site has no campaign writer. */\nexport const AI_CAMPAIGN_UNAVAILABLE_COPY = 'Campaigns are not available on this site.'\n\n/** A job's failure when a campaign whose email was written could not be drafted. */\nexport const AI_CAMPAIGN_SAVE_FAILURE_COPY =\n 'The email was written but the campaign could not be drafted. Try the job again.'\n\n/**\n * What a person is told when their plan sends no campaign email. It names the\n * page that sells the answer rather than a plan, so the one place plan names\n * are written stays the one place they are read.\n */\nexport const AI_CAMPAIGN_PLAN_REFUSAL =\n 'Campaign email is not included on this workspace’s plan. ' +\n 'Upgrade in Billing to send campaigns, or generate an email design on its own.'\n\n/** Whether this workspace may send campaign email at all: the composer's and the send route's check. */\nexport function aiCampaignPlanAllows(org: object | null): boolean {\n return checkQuota(org as Partial<AglynOrgBilling> | null, 'emailSendsPerMonth', 0).allowed\n}\n\n/** The campaign as the job's output, carrying what the person still decides. */\nexport function aiCampaignOutput(\n record: PluginDraftRecord,\n place: { hostId: string; hostSubdomain: string | null },\n note: string | null,\n): AiJobOutput {\n return {\n resource: 'campaign',\n id: record.id,\n versionId: null,\n hostId: place.hostId,\n hostSubdomain: place.hostSubdomain,\n label: record.name,\n ...(note ? { note } : {}),\n }\n}\n\n/** The subject lines and preheaders a written design already stores. */\nexport function aiCampaignCopyFromDesign(record: PluginDraftRecord): AiEmailCopy {\n const lines = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n const subject = String(record.facts?.['subject'] ?? '')\n const preheader = String(record.facts?.['preheader'] ?? '')\n const subjects = lines(record.facts?.['subjectVariants'])\n const preheaders = lines(record.facts?.['preheaderVariants'])\n return {\n subjects: subjects.length ? subjects : subject ? [subject] : [],\n preheaders: preheaders.length ? preheaders : preheader ? [preheader] : [],\n }\n}\n\n/** The campaign's content: the design it sends, and the copy the send fills its headers from. */\nexport function aiCampaignDraftContent(\n designId: string,\n copy: AiEmailCopy,\n): Record<string, unknown> {\n return {\n templateScreenId: designId,\n subject: copy.subjects[0] ?? '',\n preheader: copy.preheaders[0] ?? '',\n subjectVariants: copy.subjects,\n preheaderVariants: copy.preheaders,\n }\n}\n\n/** A campaign job's name: what the member asked for, else its strongest subject line. */\nexport function aiCampaignName(job: Pick<AiJob, 'inputs'>, copy: AiEmailCopy): string {\n return aiJobInputName(job) || copy.subjects[0] || AI_JOB_CAMPAIGN_DEFAULT_NAME\n}\n\n/**\n * A campaign job is admitted for a site of its org with email and marketing\n * on, room for a design and a campaign, and a plan that sends campaign email.\n */\nexport const aiCampaignJobAdmission: AiJobAdmission = (context) =>\n aiPluginDraftAdmissionRefusal(context, {\n kind: 'campaign',\n drafts: [\n { resource: AI_EMAIL_DESIGN_RESOURCE, pluginId: AI_EMAIL_PLUGIN_ID, label: 'Email' },\n { resource: AI_CAMPAIGN_RESOURCE, pluginId: AI_CAMPAIGN_PLUGIN_ID, label: 'Marketing' },\n ],\n ownCheck: async () =>\n aiCampaignPlanAllows(context.org)\n ? null\n : { status: 403, error: AI_CAMPAIGN_PLAN_REFUSAL },\n })\n\nexport interface AiJobCampaignStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** How a resource's draft writer is found; the core's registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n}\n\n/** Both drafts, or the refusal that stands in for a writer the site has not got. */\nfunction writers(\n writerFor: AiPluginDraftWriterLookup,\n): { design: PluginResourceDraftWriter; campaign: PluginResourceDraftWriter } | string {\n const design = writerFor(AI_EMAIL_DESIGN_RESOURCE)\n const campaign = writerFor(AI_CAMPAIGN_RESOURCE)\n if (!design) return AI_CAMPAIGN_UNAVAILABLE_COPY\n if (!campaign) return AI_CAMPAIGN_UNAVAILABLE_COPY\n return { design, campaign }\n}\n\nexport function createAiJobCampaignStep(deps: AiJobCampaignStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const writerFor = deps.writerFor ?? aiPluginDraftWriter\n return async ({ job, now, signal, firestore, modelFor }): Promise<AiJobStepOutcome> => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a campaign job names no site, and its admission refuses one')\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.campaign') ?? aiModelForStep('job.campaign')\n const found = writers(writerFor)\n if (typeof found === 'string') return { ...aiUnspentOutcome(model), failure: found }\n const { design, campaign } = found\n\n // What an earlier run of this same job already wrote, under the ids the job recorded.\n const designId = aiJobDraftId(job, 'email')\n const [writtenDesign, writtenCampaign, hostSubdomain] = await Promise.all([\n design.read({ hostId, id: designId }),\n campaign.read({ hostId, id: aiJobDraftId(job, 'campaign') }),\n aiSiteSubdomain(firestore, hostId),\n ])\n const place = { hostId, hostSubdomain }\n if (writtenDesign && writtenCampaign) {\n return aiUnspentOutcome(model, {\n outputs: [\n aiEmailDesignOutput(writtenDesign, place, null),\n aiCampaignOutput(writtenCampaign, place, null),\n ],\n })\n }\n\n const orgSnapshot = await firestore.collection('orgs').doc(job.orgId).get()\n const org = (orgSnapshot.data() ?? null) as Record<string, unknown> | null\n // Asked again here because a plan can change between admission and the run.\n if (!aiCampaignPlanAllows(org)) {\n return aiUnspentOutcome(model, { review: aiLimitReview(AI_CAMPAIGN_PLAN_REFUSAL) })\n }\n const context = { orgId: job.orgId, hostId, uid: job.createdBy, org, now }\n\n // Who it is for and when to send it: read from the site, told to the\n // person, and never sent to the model.\n const list = await suggestAiCampaignList({ hostId, brief: job.brief })\n const sendTime = list ? await readAiListSendTime(firestore, { hostId, listId: list.id }) : null\n const note = aiCampaignAudienceNote(list, sendTime)\n\n // A design an earlier run wrote is drafted into its campaign, unspent.\n if (writtenDesign) {\n return draftCampaign({\n campaign,\n context,\n job,\n copy: aiCampaignCopyFromDesign(writtenDesign),\n designId: writtenDesign.id,\n spent: aiUnspentOutcome(model),\n outputs: [aiEmailDesignOutput(writtenDesign, place, null)],\n place,\n note,\n })\n }\n\n const [inventory, products] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n resolveAiEmailProducts(firestore, { hostId, brief: job.brief, inputs: job.inputs }),\n ])\n const refusal = await design.refusal(context)\n if (refusal) {\n return refusal.status === 403\n ? aiUnspentOutcome(model, { review: aiLimitReview(refusal.error) })\n : { ...aiUnspentOutcome(model), failure: refusal.error }\n }\n\n const asked = aiJobInputName(job)\n const generated = await generateAiEmail({\n job,\n step: 'job.campaign',\n model,\n heading: asked ? `Campaign name: ${asked}` : null,\n plan: aiConfirmedPlan(job),\n inventory,\n hostId,\n products,\n design,\n ...(signal ? { signal } : {}),\n })\n const { result } = generated\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n if (!generated.copy || !generated.nodes) return { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n\n const { copy } = generated\n const name = aiCampaignName(job, copy)\n const write = await design.write({\n ...context,\n id: designId,\n name,\n content: aiEmailDesignContent(generated.nodes, copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...spent, review: aiLimitReview(write.error) }\n : { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n }\n return draftCampaign({\n campaign,\n context,\n job,\n copy,\n designId: write.id,\n spent,\n outputs: [aiEmailDesignOutput(write, place, null)],\n place,\n note,\n })\n }\n}\n\ninterface AiCampaignDraftInput {\n campaign: PluginResourceDraftWriter\n context: { orgId: string; hostId: string; uid: string; org: Record<string, unknown> | null; now: Date }\n job: AiJob\n copy: AiEmailCopy\n designId: string\n /** What the generation cost, or an unspent outcome for a run that reached no model. */\n spent: Pick<AiJobStepOutcome, 'outputs' | 'usage' | 'estCostUsd' | 'model' | 'stopReason'>\n /** The design's output, reported beside the campaign once it is drafted. */\n outputs: AiJobOutput[]\n place: { hostId: string; hostSubdomain: string | null }\n note: string\n}\n\n/**\n * Drafts the campaign around a design that exists. A campaign that cannot be\n * drafted reports NO outputs: the design on its own is not what the job was\n * asked for, and a run that names it would read as finished.\n */\nasync function draftCampaign(input: AiCampaignDraftInput): Promise<AiJobStepOutcome> {\n const write = await input.campaign.write({\n ...input.context,\n id: aiJobDraftId(input.job, 'campaign'),\n name: aiCampaignName(input.job, input.copy),\n content: aiCampaignDraftContent(input.designId, input.copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...input.spent, review: aiLimitReview(write.error) }\n : { ...input.spent, failure: AI_CAMPAIGN_SAVE_FAILURE_COPY }\n }\n return {\n ...input.spent,\n outputs: [...input.outputs, aiCampaignOutput(write, input.place, input.note)],\n }\n}\n\nexport const runAiJobCampaignStep = createAiJobCampaignStep()\n\n/**\n * The least time one campaign step needs before it starts (AGL-3035): the\n * email generation it drafts the campaign's design with, on the campaign's own\n * routing row. Writing the draft campaign asks no model.\n */\nexport const AI_JOB_CAMPAIGN_STEP_MINIMUM_MS = AI_JOB_EMAIL_STEP_BUDGETS['job.campaign'].minimumMs\n\n/** Registers the campaign step and the check a campaign job passes before it is created or resumed. */\nexport function registerAiCampaignJob(): void {\n registerAiJobStep('campaign', runAiJobCampaignStep, { minimumMs: AI_JOB_CAMPAIGN_STEP_MINIMUM_MS })\n registerAiJobAdmission('campaign', aiCampaignJobAdmission)\n}\n"],"names":["checkQuota","aiModelForStep","readSiteInventory","aiCampaignAudienceNote","readAiListSendTime","resolveAiEmailProducts","suggestAiCampaignList","registerAiJobAdmission","aiJobDraftId","aiSiteSubdomain","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiLimitReview","aiUnspentOutcome","AI_EMAIL_DESIGN_RESOURCE","AI_EMAIL_PLUGIN_ID","AI_EMAIL_SAVE_FAILURE_COPY","AI_JOB_EMAIL_STEP_BUDGETS","aiEmailDesignContent","aiEmailDesignOutput","aiJobInputName","generateAiEmail","aiPluginDraftAdmissionRefusal","aiPluginDraftWriter","registerAiJobStep","AI_CAMPAIGN_PLUGIN_ID","AI_CAMPAIGN_RESOURCE","AI_JOB_CAMPAIGN_DEFAULT_NAME","AI_CAMPAIGN_UNAVAILABLE_COPY","AI_CAMPAIGN_SAVE_FAILURE_COPY","AI_CAMPAIGN_PLAN_REFUSAL","aiCampaignPlanAllows","org","allowed","aiCampaignOutput","record","place","note","resource","id","versionId","hostId","hostSubdomain","label","name","aiCampaignCopyFromDesign","lines","value","Array","isArray","filter","entry","subject","String","facts","preheader","subjects","preheaders","length","aiCampaignDraftContent","designId","copy","templateScreenId","subjectVariants","preheaderVariants","aiCampaignName","job","aiCampaignJobAdmission","context","kind","drafts","pluginId","ownCheck","status","error","writers","writerFor","design","campaign","createAiJobCampaignStep","deps","readInventory","now","signal","firestore","modelFor","orgSnapshot","Error","model","found","failure","writtenDesign","writtenCampaign","Promise","all","read","outputs","collection","doc","orgId","get","data","review","uid","createdBy","list","brief","sendTime","listId","draftCampaign","spent","inventory","products","inputs","refusal","asked","generated","step","heading","plan","result","refused","nodes","write","content","ok","input","runAiJobCampaignStep","AI_JOB_CAMPAIGN_STEP_MINIMUM_MS","minimumMs","registerAiCampaignJob"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-campaign-step.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 { checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n PluginDraftRecord,\n PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport type { AiJob, AiJobOutput } from '../model/ai-jobs.types'\nimport { aiModelForStep } from '../providers/routing'\nimport { readSiteInventory } from '../runtime/site-inventory'\nimport {\n aiCampaignAudienceNote,\n readAiListSendTime,\n resolveAiEmailProducts,\n suggestAiCampaignList,\n} from './ai-email-bindings'\nimport { registerAiJobAdmission, type AiJobAdmission } from './ai-job-admission'\nimport { aiJobDraftId } from './ai-job-draft-ids'\nimport { aiSiteSubdomain } from './ai-job-drafts'\nimport {\n aiConfirmedPlan,\n aiDoctrineReview,\n aiGenerationSpent,\n aiLimitReview,\n aiUnspentOutcome,\n} from './ai-job-generation'\nimport {\n AI_EMAIL_DESIGN_RESOURCE,\n AI_EMAIL_PLUGIN_ID,\n AI_EMAIL_SAVE_FAILURE_COPY,\n AI_JOB_EMAIL_STEP_BUDGETS,\n aiEmailDesignContent,\n aiEmailDesignOutput,\n aiJobInputName,\n generateAiEmail,\n type AiEmailCopy,\n} from './ai-job-email-step'\nimport {\n aiPluginDraftAdmissionRefusal,\n aiPluginDraftWriter,\n type AiPluginDraftWriterLookup,\n} from './ai-job-plugin-drafts'\nimport type { AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The campaign step (AGL-2912): one brief becomes an email design AND the\n * draft campaign that would send it.\n *\n * A campaign is an unplanned kind — there is one thing to build and a plan\n * over it would be a plan of one — so the job runs this step alone, and the\n * design it writes is the email step's, generated through `generateAiEmail`\n * so both kinds obey the same doctrine and the same email rules.\n *\n * ## Nothing is sent, and nothing is aimed\n *\n * The campaign is written `draft`, with no audience, no schedule and no\n * sender: a draft campaign is not a queued one, the scheduled processor\n * queries `status == 'scheduled'`, and the send route is the only thing that\n * mails a campaign. Choosing WHO receives it stays a person's decision, so\n * the model is never shown the org's lists, its contacts or what past sends\n * did. Code reads the list the brief names and the send time that list's own\n * history suggests, AFTER the answer, and both reach the person as the\n * output's `note` — never the prompt.\n *\n * ## Where campaign email begins\n *\n * A site whose plan sends no campaign email can still generate email designs;\n * the campaign is the part that needs somewhere to send. So the step gates on\n * the same entitlement the composer and the send route read, and stops the\n * job for a person rather than failing it. The refusal names no plan, because\n * the billing page is the one place that does.\n *\n * ## Run again and it finds its drafts\n *\n * Each write is keyed by the id the job recorded for its draft when it was\n * created, so a run cut off between them finds what it wrote: two drafts are\n * reported, a design alone is drafted into a campaign from the copy the\n * design already stores, and neither spends.\n */\n\n/** The id the marketing plugin is registered under. */\nexport const AI_CAMPAIGN_PLUGIN_ID = 'marketing'\n\n/** The resource the marketing plugin writes a draft campaign under. */\nexport const AI_CAMPAIGN_RESOURCE = 'campaign'\n\n/** A campaign's name when neither the job nor its subject lines give one. */\nexport const AI_JOB_CAMPAIGN_DEFAULT_NAME = 'New campaign'\n\n/** A job's failure when the site has no campaign writer. */\nexport const AI_CAMPAIGN_UNAVAILABLE_COPY = 'Campaigns are not available on this site.'\n\n/** A job's failure when a campaign whose email was written could not be drafted. */\nexport const AI_CAMPAIGN_SAVE_FAILURE_COPY =\n 'The email was written but the campaign could not be drafted. Try the job again.'\n\n/**\n * What a person is told when their plan sends no campaign email. It names the\n * page that sells the answer rather than a plan, so the one place plan names\n * are written stays the one place they are read.\n */\nexport const AI_CAMPAIGN_PLAN_REFUSAL =\n 'Campaign email is not included on this workspace’s plan. ' +\n 'Upgrade in Billing to send campaigns, or generate an email design on its own.'\n\n/** Whether this workspace may send campaign email at all: the composer's and the send route's check. */\nexport function aiCampaignPlanAllows(org: object | null): boolean {\n return checkQuota(org as Partial<AglynOrgBilling> | null, 'emailSendsPerMonth', 0).allowed\n}\n\n/** The campaign as the job's output, carrying what the person still decides. */\nexport function aiCampaignOutput(\n record: PluginDraftRecord,\n place: { hostId: string; hostSubdomain: string | null },\n note: string | null,\n): AiJobOutput {\n return {\n resource: 'campaign',\n id: record.id,\n versionId: null,\n hostId: place.hostId,\n hostSubdomain: place.hostSubdomain,\n label: record.name,\n ...(note ? { note } : {}),\n }\n}\n\n/** The subject lines and preheaders a written design already stores. */\nexport function aiCampaignCopyFromDesign(record: PluginDraftRecord): AiEmailCopy {\n const lines = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : []\n const subject = String(record.facts?.['subject'] ?? '')\n const preheader = String(record.facts?.['preheader'] ?? '')\n const subjects = lines(record.facts?.['subjectVariants'])\n const preheaders = lines(record.facts?.['preheaderVariants'])\n return {\n subjects: subjects.length ? subjects : subject ? [subject] : [],\n preheaders: preheaders.length ? preheaders : preheader ? [preheader] : [],\n }\n}\n\n/** The campaign's content: the design it sends, and the copy the send fills its headers from. */\nexport function aiCampaignDraftContent(\n designId: string,\n copy: AiEmailCopy,\n): Record<string, unknown> {\n return {\n templateScreenId: designId,\n subject: copy.subjects[0] ?? '',\n preheader: copy.preheaders[0] ?? '',\n subjectVariants: copy.subjects,\n preheaderVariants: copy.preheaders,\n }\n}\n\n/** A campaign job's name: what the member asked for, else its strongest subject line. */\nexport function aiCampaignName(job: Pick<AiJob, 'inputs'>, copy: AiEmailCopy): string {\n return aiJobInputName(job) || copy.subjects[0] || AI_JOB_CAMPAIGN_DEFAULT_NAME\n}\n\n/**\n * A campaign job is admitted for a site of its org with email and marketing\n * on, room for a design and a campaign, and a plan that sends campaign email.\n */\nexport const aiCampaignJobAdmission: AiJobAdmission = (context) =>\n aiPluginDraftAdmissionRefusal(context, {\n kind: 'campaign',\n drafts: [\n { resource: AI_EMAIL_DESIGN_RESOURCE, pluginId: AI_EMAIL_PLUGIN_ID, label: 'Email' },\n { resource: AI_CAMPAIGN_RESOURCE, pluginId: AI_CAMPAIGN_PLUGIN_ID, label: 'Marketing' },\n ],\n ownCheck: async () =>\n aiCampaignPlanAllows(context.org)\n ? null\n : { status: 403, error: AI_CAMPAIGN_PLAN_REFUSAL },\n })\n\nexport interface AiJobCampaignStepDeps {\n /** The inventory reader; specs hand in a fake. */\n readInventory?: typeof readSiteInventory\n /** How a resource's draft writer is found; the core's registry otherwise. */\n writerFor?: AiPluginDraftWriterLookup\n}\n\n/** Both drafts, or the refusal that stands in for a writer the site has not got. */\nfunction writers(\n writerFor: AiPluginDraftWriterLookup,\n): { design: PluginResourceDraftWriter; campaign: PluginResourceDraftWriter } | string {\n const design = writerFor(AI_EMAIL_DESIGN_RESOURCE)\n const campaign = writerFor(AI_CAMPAIGN_RESOURCE)\n if (!design) return AI_CAMPAIGN_UNAVAILABLE_COPY\n if (!campaign) return AI_CAMPAIGN_UNAVAILABLE_COPY\n return { design, campaign }\n}\n\nexport function createAiJobCampaignStep(deps: AiJobCampaignStepDeps = {}): AiJobStepRunner {\n const readInventory = deps.readInventory ?? readSiteInventory\n const writerFor = deps.writerFor ?? aiPluginDraftWriter\n return async ({ job, now, signal, firestore, modelFor }): Promise<AiJobStepOutcome> => {\n const hostId = job.hostId\n if (!hostId) throw new Error('a campaign job names no site, and its admission refuses one')\n // The switch's answer for this job, else the routing table's (AGL-2942).\n const model = modelFor?.('job.campaign') ?? aiModelForStep('job.campaign')\n const found = writers(writerFor)\n if (typeof found === 'string') return { ...aiUnspentOutcome(model), failure: found }\n const { design, campaign } = found\n\n // What an earlier run of this same job already wrote, under the ids the job recorded.\n const designId = aiJobDraftId(job, 'email')\n const [writtenDesign, writtenCampaign, hostSubdomain] = await Promise.all([\n design.read({ hostId, id: designId }),\n campaign.read({ hostId, id: aiJobDraftId(job, 'campaign') }),\n aiSiteSubdomain(firestore, hostId),\n ])\n const place = { hostId, hostSubdomain }\n if (writtenDesign && writtenCampaign) {\n return aiUnspentOutcome(model, {\n outputs: [\n aiEmailDesignOutput(writtenDesign, place, null),\n aiCampaignOutput(writtenCampaign, place, null),\n ],\n })\n }\n\n const orgSnapshot = await firestore.collection('orgs').doc(job.orgId).get()\n const org = (orgSnapshot.data() ?? null) as Record<string, unknown> | null\n // Asked again here because a plan can change between admission and the run.\n if (!aiCampaignPlanAllows(org)) {\n return aiUnspentOutcome(model, { review: aiLimitReview(AI_CAMPAIGN_PLAN_REFUSAL) })\n }\n const context = { orgId: job.orgId, hostId, uid: job.createdBy, org, now }\n\n // Who it is for and when to send it: read from the site, told to the\n // person, and never sent to the model.\n const list = await suggestAiCampaignList({ hostId, brief: job.brief })\n const sendTime = list ? await readAiListSendTime(firestore, { orgId: job.orgId, hostId, listId: list.id }) : null\n const note = aiCampaignAudienceNote(list, sendTime)\n\n // A design an earlier run wrote is drafted into its campaign, unspent.\n if (writtenDesign) {\n return draftCampaign({\n campaign,\n context,\n job,\n copy: aiCampaignCopyFromDesign(writtenDesign),\n designId: writtenDesign.id,\n spent: aiUnspentOutcome(model),\n outputs: [aiEmailDesignOutput(writtenDesign, place, null)],\n place,\n note,\n })\n }\n\n const [inventory, products] = await Promise.all([\n readInventory(job.orgId, hostId, { firestore }),\n resolveAiEmailProducts(firestore, { hostId, brief: job.brief, inputs: job.inputs }),\n ])\n const refusal = await design.refusal(context)\n if (refusal) {\n return refusal.status === 403\n ? aiUnspentOutcome(model, { review: aiLimitReview(refusal.error) })\n : { ...aiUnspentOutcome(model), failure: refusal.error }\n }\n\n const asked = aiJobInputName(job)\n const generated = await generateAiEmail({\n job,\n step: 'job.campaign',\n model,\n heading: asked ? `Campaign name: ${asked}` : null,\n plan: aiConfirmedPlan(job),\n inventory,\n hostId,\n products,\n design,\n ...(signal ? { signal } : {}),\n })\n const { result } = generated\n const spent = aiGenerationSpent(result)\n if (result.status === 'refused') return { ...spent, refused: true }\n if (result.status === 'needs_input') return { ...spent, review: aiDoctrineReview(result) }\n if (!generated.copy || !generated.nodes) return { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n\n const { copy } = generated\n const name = aiCampaignName(job, copy)\n const write = await design.write({\n ...context,\n id: designId,\n name,\n content: aiEmailDesignContent(generated.nodes, copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...spent, review: aiLimitReview(write.error) }\n : { ...spent, failure: AI_EMAIL_SAVE_FAILURE_COPY }\n }\n return draftCampaign({\n campaign,\n context,\n job,\n copy,\n designId: write.id,\n spent,\n outputs: [aiEmailDesignOutput(write, place, null)],\n place,\n note,\n })\n }\n}\n\ninterface AiCampaignDraftInput {\n campaign: PluginResourceDraftWriter\n context: { orgId: string; hostId: string; uid: string; org: Record<string, unknown> | null; now: Date }\n job: AiJob\n copy: AiEmailCopy\n designId: string\n /** What the generation cost, or an unspent outcome for a run that reached no model. */\n spent: Pick<AiJobStepOutcome, 'outputs' | 'usage' | 'estCostUsd' | 'model' | 'stopReason'>\n /** The design's output, reported beside the campaign once it is drafted. */\n outputs: AiJobOutput[]\n place: { hostId: string; hostSubdomain: string | null }\n note: string\n}\n\n/**\n * Drafts the campaign around a design that exists. A campaign that cannot be\n * drafted reports NO outputs: the design on its own is not what the job was\n * asked for, and a run that names it would read as finished.\n */\nasync function draftCampaign(input: AiCampaignDraftInput): Promise<AiJobStepOutcome> {\n const write = await input.campaign.write({\n ...input.context,\n id: aiJobDraftId(input.job, 'campaign'),\n name: aiCampaignName(input.job, input.copy),\n content: aiCampaignDraftContent(input.designId, input.copy),\n })\n if (write.ok === false) {\n return write.status === 403\n ? { ...input.spent, review: aiLimitReview(write.error) }\n : { ...input.spent, failure: AI_CAMPAIGN_SAVE_FAILURE_COPY }\n }\n return {\n ...input.spent,\n outputs: [...input.outputs, aiCampaignOutput(write, input.place, input.note)],\n }\n}\n\nexport const runAiJobCampaignStep = createAiJobCampaignStep()\n\n/**\n * The least time one campaign step needs before it starts (AGL-3035): the\n * email generation it drafts the campaign's design with, on the campaign's own\n * routing row. Writing the draft campaign asks no model.\n */\nexport const AI_JOB_CAMPAIGN_STEP_MINIMUM_MS = AI_JOB_EMAIL_STEP_BUDGETS['job.campaign'].minimumMs\n\n/** Registers the campaign step and the check a campaign job passes before it is created or resumed. */\nexport function registerAiCampaignJob(): void {\n registerAiJobStep('campaign', runAiJobCampaignStep, { minimumMs: AI_JOB_CAMPAIGN_STEP_MINIMUM_MS })\n registerAiJobAdmission('campaign', aiCampaignJobAdmission)\n}\n"],"names":["checkQuota","aiModelForStep","readSiteInventory","aiCampaignAudienceNote","readAiListSendTime","resolveAiEmailProducts","suggestAiCampaignList","registerAiJobAdmission","aiJobDraftId","aiSiteSubdomain","aiConfirmedPlan","aiDoctrineReview","aiGenerationSpent","aiLimitReview","aiUnspentOutcome","AI_EMAIL_DESIGN_RESOURCE","AI_EMAIL_PLUGIN_ID","AI_EMAIL_SAVE_FAILURE_COPY","AI_JOB_EMAIL_STEP_BUDGETS","aiEmailDesignContent","aiEmailDesignOutput","aiJobInputName","generateAiEmail","aiPluginDraftAdmissionRefusal","aiPluginDraftWriter","registerAiJobStep","AI_CAMPAIGN_PLUGIN_ID","AI_CAMPAIGN_RESOURCE","AI_JOB_CAMPAIGN_DEFAULT_NAME","AI_CAMPAIGN_UNAVAILABLE_COPY","AI_CAMPAIGN_SAVE_FAILURE_COPY","AI_CAMPAIGN_PLAN_REFUSAL","aiCampaignPlanAllows","org","allowed","aiCampaignOutput","record","place","note","resource","id","versionId","hostId","hostSubdomain","label","name","aiCampaignCopyFromDesign","lines","value","Array","isArray","filter","entry","subject","String","facts","preheader","subjects","preheaders","length","aiCampaignDraftContent","designId","copy","templateScreenId","subjectVariants","preheaderVariants","aiCampaignName","job","aiCampaignJobAdmission","context","kind","drafts","pluginId","ownCheck","status","error","writers","writerFor","design","campaign","createAiJobCampaignStep","deps","readInventory","now","signal","firestore","modelFor","orgSnapshot","Error","model","found","failure","writtenDesign","writtenCampaign","Promise","all","read","outputs","collection","doc","orgId","get","data","review","uid","createdBy","list","brief","sendTime","listId","draftCampaign","spent","inventory","products","inputs","refusal","asked","generated","step","heading","plan","result","refused","nodes","write","content","ok","input","runAiJobCampaignStep","AI_JOB_CAMPAIGN_STEP_MINIMUM_MS","minimumMs","registerAiCampaignJob"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/ import { actionRunResult } from "@aglyn/aglyn/app-utils/activity-presenter";
|
|
17
17
|
import { datasetDisplayName } from "@aglyn/aglyn/app-utils/datasets";
|
|
18
18
|
import { isFormArchived } from "@aglyn/aglyn/app-utils/forms";
|
|
19
|
+
import { campaignPlacedOnHost } from "@aglyn/shared-ui-email-campaigns/model/campaign-container";
|
|
19
20
|
import { scopedToHost } from "@aglyn/tenant-data-admin/server/organizations";
|
|
20
21
|
/** Records of one kind a lookup reads: the Actions editor's picker window. */ export const AI_WORKFLOW_RECORDS_WINDOW = 100;
|
|
21
22
|
/** Pipelines a stage lookup reads. */ export const AI_WORKFLOW_PIPELINES_WINDOW = 20;
|
|
@@ -72,10 +73,15 @@ const live = (data)=>data['deletedAt'] == null;
|
|
|
72
73
|
'name',
|
|
73
74
|
'deletedAt'
|
|
74
75
|
], live, (data)=>text(data['name'])),
|
|
75
|
-
|
|
76
|
+
// The org's campaigns placed on this site: the set the automation's
|
|
77
|
+
// `assignCampaign` step accepts when it runs here.
|
|
78
|
+
!wanted('campaigns') ? none : named(org.collection('emailCampaigns'), [
|
|
76
79
|
'name',
|
|
77
|
-
'deletedAt'
|
|
78
|
-
|
|
80
|
+
'deletedAt',
|
|
81
|
+
'visibleTo'
|
|
82
|
+
], (data)=>live(data) && campaignPlacedOnHost({
|
|
83
|
+
visibleTo: data['visibleTo']
|
|
84
|
+
}, input.hostId), (data)=>text(data['name'])),
|
|
79
85
|
!wanted('workflows') ? none : named(host.collection('workflows'), [
|
|
80
86
|
'name',
|
|
81
87
|
'deletedAt'
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-workflow-records.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 type { HostAction } from '@aglyn/aglyn/app-utils/actions'\nimport { actionRunResult } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { datasetDisplayName } from '@aglyn/aglyn/app-utils/datasets'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport type { HostWorkflow } from '@aglyn/aglyn/app-utils/workflows'\nimport { scopedToHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type {\n AiAutomationForm,\n AiAutomationNamedRecord,\n AiAutomationRecords,\n} from '../model/ai-automation-draft'\nimport type { AiRunRecord } from '../model/ai-automation-outline'\nimport type { AiWorkflowTargetType } from '../model/ai-workflow-job'\n\n/**\n * What a `workflow` job reads (AGL-2919), through the Admin SDK and scoped to\n * the job's own site and org: the records a drafted automation's words are\n * looked up among, the automation an explanation reads, and the run it\n * explains. Every read is a projection of the fields named here, windowed the\n * way the Actions editor's pickers are, so a lookup never reads more than the\n * editor would offer.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\n/** Records of one kind a lookup reads: the Actions editor's picker window. */\nexport const AI_WORKFLOW_RECORDS_WINDOW = 100\n\n/** Pipelines a stage lookup reads. */\nexport const AI_WORKFLOW_PIPELINES_WINDOW = 20\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nasync function named(\n query: FirebaseFirestore.Query,\n fields: string[],\n keep: (data: Data) => boolean,\n nameOf: (data: Data, id: string) => string,\n): Promise<AiAutomationNamedRecord[]> {\n const snapshot = await query\n .select(...fields)\n .limit(AI_WORKFLOW_RECORDS_WINDOW)\n .get()\n return snapshot.docs\n .map((doc) => ({ data: (doc.data() ?? {}) as Data, id: doc.id }))\n .filter(({ data }) => keep(data))\n .map(({ data, id }) => ({ id, name: nameOf(data, id) }))\n .filter((record) => record.name)\n}\n\nconst live = (data: Data) => data['deletedAt'] == null\n\n/** The site's forms that are not archived, with the field names their submissions carry. */\nasync function readForms(host: FirebaseFirestore.DocumentReference): Promise<AiAutomationForm[]> {\n const snapshot = await host\n .collection('forms')\n .select('displayName', 'slug', 'fields', 'archivedAt')\n .limit(AI_WORKFLOW_RECORDS_WINDOW)\n .get()\n return snapshot.docs\n .filter((doc) => !isFormArchived((doc.data() ?? {}) as { archivedAt?: unknown }))\n .map((doc) => {\n const data = (doc.data() ?? {}) as Data\n return {\n id: doc.id,\n name: text(data['displayName']) || text(data['slug']) || doc.id,\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n }\n })\n}\n\n/**\n * The site's records a drafted automation may name. Stages are read only for\n * a workspace with the CRM, whose pipelines they are; a site without it has\n * no stage a condition could name. `only`, when given, names the kinds to\n * read, and every other kind is answered empty without a read.\n */\nexport async function readAiAutomationRecords(\n firestore: Firestore,\n input: { orgId: string; hostId: string; crm: boolean; only?: ReadonlyArray<keyof AiAutomationRecords> },\n): Promise<AiAutomationRecords> {\n const host = firestore.collection('hosts').doc(input.hostId)\n const org = firestore.collection('orgs').doc(input.orgId)\n const wanted = (kind: keyof AiAutomationRecords) => !input.only || input.only.includes(kind)\n const none = Promise.resolve([] as AiAutomationNamedRecord[])\n const [forms, datasets, lists, campaigns, workflows, webhooks, stages] = await Promise.all([\n !wanted('forms') ? Promise.resolve([] as AiAutomationForm[]) : readForms(host),\n !wanted('datasets')\n ? none\n : named(\n scopedToHost(org.collection('datasets'), input.hostId),\n ['displayName', 'name', 'deletedAt'],\n live,\n (data, id) => datasetDisplayName(data) || id,\n ),\n !wanted('lists') ? none : named(org.collection('lists'), ['name', 'deletedAt'], live, (data) => text(data['name'])),\n !wanted('campaigns')\n ? none\n : named(host.collection('emailCampaigns'), ['name', 'deletedAt'], live, (data) => text(data['name'])),\n !wanted('workflows')\n ? none\n : named(host.collection('workflows'), ['name', 'deletedAt'], live, (data) => text(data['name'])),\n !wanted('webhooks')\n ? none\n : named(\n host.collection('webhooks'),\n ['name', 'direction', 'deletedAt'],\n (data) => live(data) && data['direction'] === 'outbound',\n (data) => text(data['name']),\n ),\n input.crm && wanted('stages')\n ? scopedToHost(org.collection('pipelines'), input.hostId)\n .select('stages', 'archivedAt')\n .limit(AI_WORKFLOW_PIPELINES_WINDOW)\n .get()\n .then((snapshot) =>\n snapshot.docs\n .filter((doc) => !(Number(doc.get('archivedAt')) > 0))\n .flatMap((doc) => (Array.isArray(doc.get('stages')) ? (doc.get('stages') as Data[]) : []))\n .map((stage) => ({ id: text(stage?.['id']), name: text(stage?.['name']) }))\n .filter((stage) => stage.id && stage.name),\n )\n : none,\n ])\n return { forms, datasets, lists, campaigns, workflows, webhooks, stages }\n}\n\n/** The site's functions, for a workflow outline that says which of its calls still resolve. */\nexport async function readAiWorkflowFunctions(\n firestore: Firestore,\n hostId: string,\n): Promise<AiAutomationNamedRecord[]> {\n return named(\n firestore.collection('hosts').doc(hostId).collection('functions'),\n ['name', 'deletedAt'],\n live,\n (data) => text(data['name']),\n )\n}\n\nexport type AiWorkflowTarget =\n | { type: 'action'; id: string; name: string; action: HostAction }\n | { type: 'workflow'; id: string; name: string; workflow: HostWorkflow }\n\n/** A saved automation of the site, or `null` for one that does not exist or was deleted. */\nexport async function readAiWorkflowTarget(\n firestore: Firestore,\n input: { hostId: string; type: AiWorkflowTargetType; id: string },\n): Promise<AiWorkflowTarget | null> {\n const collection = input.type === 'action' ? 'actions' : 'workflows'\n const snapshot = await firestore.collection('hosts').doc(input.hostId).collection(collection).doc(input.id).get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n if (!data || data['deletedAt'] != null) return null\n const name = text(data['name']) || input.id\n return input.type === 'action'\n ? { type: 'action', id: snapshot.id, name, action: data as unknown as HostAction }\n : { type: 'workflow', id: snapshot.id, name, workflow: data as unknown as HostWorkflow }\n}\n\nexport type AiWorkflowRunRead =\n | { ok: true; run: AiRunRecord }\n | { ok: false; reason: 'gone' | 'not-failed' }\n\n/**\n * One run of an automation, as the site's activity log recorded it: gone when\n * there is no such entry or it belongs to another automation, and refused\n * when it did not fail. Only what the run history shows is read — never the\n * event's payload.\n */\nexport async function readAiWorkflowRun(\n firestore: Firestore,\n input: { hostId: string; targetId: string; runId: string },\n): Promise<AiWorkflowRunRead> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(input.hostId)\n .collection('activity')\n .doc(input.runId)\n .get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n const target = data?.['target'] as { id?: unknown } | undefined\n if (!data || target?.id !== input.targetId) return { ok: false, reason: 'gone' }\n const run: AiRunRecord = {\n result: data['result'],\n trigger: data['trigger'],\n summary: data['summary'],\n action: data['action'],\n createdAt: data['createdAt'],\n }\n return actionRunResult(run as never) === 'failed' ? { ok: true, run } : { ok: false, reason: 'not-failed' }\n}\n"],"names":["actionRunResult","datasetDisplayName","isFormArchived","scopedToHost","AI_WORKFLOW_RECORDS_WINDOW","AI_WORKFLOW_PIPELINES_WINDOW","text","value","trim","named","query","fields","keep","nameOf","snapshot","select","limit","get","docs","map","doc","data","id","filter","name","record","live","readForms","host","collection","Array","isArray","field","Boolean","readAiAutomationRecords","firestore","input","hostId","org","orgId","wanted","kind","only","includes","none","Promise","resolve","forms","datasets","lists","campaigns","workflows","webhooks","stages","all","crm","then","Number","flatMap","stage","readAiWorkflowFunctions","readAiWorkflowTarget","type","exists","action","workflow","readAiWorkflowRun","runId","target","targetId","ok","reason","run","result","trigger","summary","createdAt"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-workflow-records.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 type { HostAction } from '@aglyn/aglyn/app-utils/actions'\nimport { actionRunResult } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { datasetDisplayName } from '@aglyn/aglyn/app-utils/datasets'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport type { HostWorkflow } from '@aglyn/aglyn/app-utils/workflows'\nimport { campaignPlacedOnHost } from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport { scopedToHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type {\n AiAutomationForm,\n AiAutomationNamedRecord,\n AiAutomationRecords,\n} from '../model/ai-automation-draft'\nimport type { AiRunRecord } from '../model/ai-automation-outline'\nimport type { AiWorkflowTargetType } from '../model/ai-workflow-job'\n\n/**\n * What a `workflow` job reads (AGL-2919), through the Admin SDK and scoped to\n * the job's own site and org: the records a drafted automation's words are\n * looked up among, the automation an explanation reads, and the run it\n * explains. Every read is a projection of the fields named here, windowed the\n * way the Actions editor's pickers are, so a lookup never reads more than the\n * editor would offer.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\n/** Records of one kind a lookup reads: the Actions editor's picker window. */\nexport const AI_WORKFLOW_RECORDS_WINDOW = 100\n\n/** Pipelines a stage lookup reads. */\nexport const AI_WORKFLOW_PIPELINES_WINDOW = 20\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nasync function named(\n query: FirebaseFirestore.Query,\n fields: string[],\n keep: (data: Data) => boolean,\n nameOf: (data: Data, id: string) => string,\n): Promise<AiAutomationNamedRecord[]> {\n const snapshot = await query\n .select(...fields)\n .limit(AI_WORKFLOW_RECORDS_WINDOW)\n .get()\n return snapshot.docs\n .map((doc) => ({ data: (doc.data() ?? {}) as Data, id: doc.id }))\n .filter(({ data }) => keep(data))\n .map(({ data, id }) => ({ id, name: nameOf(data, id) }))\n .filter((record) => record.name)\n}\n\nconst live = (data: Data) => data['deletedAt'] == null\n\n/** The site's forms that are not archived, with the field names their submissions carry. */\nasync function readForms(host: FirebaseFirestore.DocumentReference): Promise<AiAutomationForm[]> {\n const snapshot = await host\n .collection('forms')\n .select('displayName', 'slug', 'fields', 'archivedAt')\n .limit(AI_WORKFLOW_RECORDS_WINDOW)\n .get()\n return snapshot.docs\n .filter((doc) => !isFormArchived((doc.data() ?? {}) as { archivedAt?: unknown }))\n .map((doc) => {\n const data = (doc.data() ?? {}) as Data\n return {\n id: doc.id,\n name: text(data['displayName']) || text(data['slug']) || doc.id,\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n }\n })\n}\n\n/**\n * The site's records a drafted automation may name. Stages are read only for\n * a workspace with the CRM, whose pipelines they are; a site without it has\n * no stage a condition could name. `only`, when given, names the kinds to\n * read, and every other kind is answered empty without a read.\n */\nexport async function readAiAutomationRecords(\n firestore: Firestore,\n input: { orgId: string; hostId: string; crm: boolean; only?: ReadonlyArray<keyof AiAutomationRecords> },\n): Promise<AiAutomationRecords> {\n const host = firestore.collection('hosts').doc(input.hostId)\n const org = firestore.collection('orgs').doc(input.orgId)\n const wanted = (kind: keyof AiAutomationRecords) => !input.only || input.only.includes(kind)\n const none = Promise.resolve([] as AiAutomationNamedRecord[])\n const [forms, datasets, lists, campaigns, workflows, webhooks, stages] = await Promise.all([\n !wanted('forms') ? Promise.resolve([] as AiAutomationForm[]) : readForms(host),\n !wanted('datasets')\n ? none\n : named(\n scopedToHost(org.collection('datasets'), input.hostId),\n ['displayName', 'name', 'deletedAt'],\n live,\n (data, id) => datasetDisplayName(data) || id,\n ),\n !wanted('lists') ? none : named(org.collection('lists'), ['name', 'deletedAt'], live, (data) => text(data['name'])),\n // The org's campaigns placed on this site: the set the automation's\n // `assignCampaign` step accepts when it runs here.\n !wanted('campaigns')\n ? none\n : named(\n org.collection('emailCampaigns'),\n ['name', 'deletedAt', 'visibleTo'],\n (data) =>\n live(data) &&\n campaignPlacedOnHost({ visibleTo: data['visibleTo'] as string[] | undefined }, input.hostId),\n (data) => text(data['name']),\n ),\n !wanted('workflows')\n ? none\n : named(host.collection('workflows'), ['name', 'deletedAt'], live, (data) => text(data['name'])),\n !wanted('webhooks')\n ? none\n : named(\n host.collection('webhooks'),\n ['name', 'direction', 'deletedAt'],\n (data) => live(data) && data['direction'] === 'outbound',\n (data) => text(data['name']),\n ),\n input.crm && wanted('stages')\n ? scopedToHost(org.collection('pipelines'), input.hostId)\n .select('stages', 'archivedAt')\n .limit(AI_WORKFLOW_PIPELINES_WINDOW)\n .get()\n .then((snapshot) =>\n snapshot.docs\n .filter((doc) => !(Number(doc.get('archivedAt')) > 0))\n .flatMap((doc) => (Array.isArray(doc.get('stages')) ? (doc.get('stages') as Data[]) : []))\n .map((stage) => ({ id: text(stage?.['id']), name: text(stage?.['name']) }))\n .filter((stage) => stage.id && stage.name),\n )\n : none,\n ])\n return { forms, datasets, lists, campaigns, workflows, webhooks, stages }\n}\n\n/** The site's functions, for a workflow outline that says which of its calls still resolve. */\nexport async function readAiWorkflowFunctions(\n firestore: Firestore,\n hostId: string,\n): Promise<AiAutomationNamedRecord[]> {\n return named(\n firestore.collection('hosts').doc(hostId).collection('functions'),\n ['name', 'deletedAt'],\n live,\n (data) => text(data['name']),\n )\n}\n\nexport type AiWorkflowTarget =\n | { type: 'action'; id: string; name: string; action: HostAction }\n | { type: 'workflow'; id: string; name: string; workflow: HostWorkflow }\n\n/** A saved automation of the site, or `null` for one that does not exist or was deleted. */\nexport async function readAiWorkflowTarget(\n firestore: Firestore,\n input: { hostId: string; type: AiWorkflowTargetType; id: string },\n): Promise<AiWorkflowTarget | null> {\n const collection = input.type === 'action' ? 'actions' : 'workflows'\n const snapshot = await firestore.collection('hosts').doc(input.hostId).collection(collection).doc(input.id).get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n if (!data || data['deletedAt'] != null) return null\n const name = text(data['name']) || input.id\n return input.type === 'action'\n ? { type: 'action', id: snapshot.id, name, action: data as unknown as HostAction }\n : { type: 'workflow', id: snapshot.id, name, workflow: data as unknown as HostWorkflow }\n}\n\nexport type AiWorkflowRunRead =\n | { ok: true; run: AiRunRecord }\n | { ok: false; reason: 'gone' | 'not-failed' }\n\n/**\n * One run of an automation, as the site's activity log recorded it: gone when\n * there is no such entry or it belongs to another automation, and refused\n * when it did not fail. Only what the run history shows is read — never the\n * event's payload.\n */\nexport async function readAiWorkflowRun(\n firestore: Firestore,\n input: { hostId: string; targetId: string; runId: string },\n): Promise<AiWorkflowRunRead> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(input.hostId)\n .collection('activity')\n .doc(input.runId)\n .get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n const target = data?.['target'] as { id?: unknown } | undefined\n if (!data || target?.id !== input.targetId) return { ok: false, reason: 'gone' }\n const run: AiRunRecord = {\n result: data['result'],\n trigger: data['trigger'],\n summary: data['summary'],\n action: data['action'],\n createdAt: data['createdAt'],\n }\n return actionRunResult(run as never) === 'failed' ? { ok: true, run } : { ok: false, reason: 'not-failed' }\n}\n"],"names":["actionRunResult","datasetDisplayName","isFormArchived","campaignPlacedOnHost","scopedToHost","AI_WORKFLOW_RECORDS_WINDOW","AI_WORKFLOW_PIPELINES_WINDOW","text","value","trim","named","query","fields","keep","nameOf","snapshot","select","limit","get","docs","map","doc","data","id","filter","name","record","live","readForms","host","collection","Array","isArray","field","Boolean","readAiAutomationRecords","firestore","input","hostId","org","orgId","wanted","kind","only","includes","none","Promise","resolve","forms","datasets","lists","campaigns","workflows","webhooks","stages","all","visibleTo","crm","then","Number","flatMap","stage","readAiWorkflowFunctions","readAiWorkflowTarget","type","exists","action","workflow","readAiWorkflowRun","runId","target","targetId","ok","reason","run","result","trigger","summary","createdAt"],"mappings":"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"}
|