@aglyn/plugins-ai 1.0.0-beta.261 → 1.0.0-beta.263
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-brief-dialog.component.d.ts +21 -58
- package/src/lib/components/ai-brief-dialog.component.js +8 -132
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-follow.component.js +2 -35
- package/src/lib/components/ai-job-follow.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +2 -8
- package/src/lib/components/ai-job-plan.component.js +13 -74
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-template-collection-field.component.d.ts +2 -12
- package/src/lib/components/ai-template-collection-field.component.js +13 -10
- package/src/lib/components/ai-template-collection-field.component.js.map +1 -1
- package/src/lib/components/ai-upsell-dialog.component.d.ts +3 -25
- package/src/lib/components/ai-upsell-dialog.component.js +9 -66
- package/src/lib/components/ai-upsell-dialog.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.d.ts +3 -8
- package/src/lib/components/use-ai-job-run.js +4 -15
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/insights/ai-figure-readers.js +15 -5
- package/src/lib/insights/ai-figure-readers.js.map +1 -1
- package/src/lib/insights/ai-figure-summaries.d.ts +27 -0
- package/src/lib/insights/ai-figure-summaries.js +96 -0
- package/src/lib/insights/ai-figure-summaries.js.map +1 -0
- package/src/lib/insights/ai-insight-digest.d.ts +2 -0
- package/src/lib/insights/ai-insight-digest.js +7 -4
- package/src/lib/insights/ai-insight-digest.js.map +1 -1
- package/src/lib/insights/ai-insight-readers.d.ts +2 -1
- package/src/lib/insights/ai-insight-readers.js +11 -26
- package/src/lib/insights/ai-insight-readers.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +5 -3
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-page-language.js +23 -2
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +14 -1
- package/src/lib/jobs/ai-job-plan-step.js +44 -9
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +8 -1
- package/src/lib/jobs/ai-jobs-notify.js +44 -2
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.js +11 -1
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/layout-language/ai-layout-compiler.js +18 -7
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-promised-copy.d.ts +54 -0
- package/src/lib/layout-language/ai-layout-promised-copy.js +104 -0
- package/src/lib/layout-language/ai-layout-promised-copy.js.map +1 -0
- package/src/lib/model/ai-brief-copy.d.ts +85 -0
- package/src/lib/model/ai-brief-copy.js +152 -0
- package/src/lib/model/ai-brief-copy.js.map +1 -0
- package/src/lib/model/ai-create-view.d.ts +200 -0
- package/src/lib/model/ai-create-view.js +263 -0
- package/src/lib/model/ai-create-view.js.map +1 -0
- package/src/lib/model/ai-insight.d.ts +10 -1
- package/src/lib/model/ai-insight.js +16 -39
- package/src/lib/model/ai-insight.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +24 -0
- package/src/lib/model/ai-job-activity.js +52 -1
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-job-notice.d.ts +7 -1
- package/src/lib/model/ai-job-notice.js +10 -9
- package/src/lib/model/ai-job-notice.js.map +1 -1
- package/src/lib/model/ai-job-plan-lines.d.ts +46 -0
- package/src/lib/model/ai-job-plan-lines.js +70 -0
- package/src/lib/model/ai-job-plan-lines.js.map +1 -0
- package/src/lib/model/ai-plan-said-sections.d.ts +81 -0
- package/src/lib/model/ai-plan-said-sections.js +168 -0
- package/src/lib/model/ai-plan-said-sections.js.map +1 -0
- package/src/lib/model/ai-site-job.d.ts +8 -2
- package/src/lib/model/ai-site-job.js +7 -1
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-template-collections.d.ts +28 -0
- package/src/lib/model/ai-template-collections.js +28 -0
- package/src/lib/model/ai-template-collections.js.map +1 -0
- package/src/lib/model/ai-upsell-copy.d.ts +41 -0
- package/src/lib/model/ai-upsell-copy.js +85 -0
- package/src/lib/model/ai-upsell-copy.js.map +1 -0
- package/src/lib/runtime/ai-doctrine-validators.d.ts +15 -1
- package/src/lib/runtime/ai-doctrine-validators.js +96 -3
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-eval-memory-firestore.js +2 -0
- package/src/lib/runtime/ai-eval-memory-firestore.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +11 -3
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-create-route.d.ts +32 -0
- package/src/lib/server/ai-create-route.js +295 -0
- package/src/lib/server/ai-create-route.js.map +1 -0
- package/src/lib/server.js +14 -0
- package/src/lib/server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-digest.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 { FieldPath } from 'firebase-admin/firestore'\nimport { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed, notificationMuted } from '@aglyn/aglyn/app-utils/notifications'\nimport { hostRoleFor } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, resolveBrandingProfile, resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides, type ReleaseFlagValue } from '@aglyn/aglyn/app-utils/release-flags'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport {\n AI_INSIGHTS_COLLECTION,\n aiInsightIsoWeek,\n type AiInsightRecord,\n} from '../model/ai-insight'\nimport { isAiOnForSite } from '../model/ai-site-switch'\nimport { cancelAiJob, createAiJob, getAiJob } from '../jobs/ai-jobs'\n\n/**\n * THE WEEKLY INSIGHTS (AGL-2915): `POST /api/admin/ai-insights-digest`, twice\n * a day on the console's scheduler.\n *\n * ## Who asked for it\n *\n * A person turns weekly insights on for a workspace beside the answers\n * themselves (`users/{uid}.insightDigests.{orgId}`), and off there or in\n * Notifications. It is opt-in, because each digest is a generation the\n * workspace pays for. At the sweep, every rung is asked again rather than\n * trusted from the switch: the workspace's plan includes `aiGenerative`,\n * `release_ai_generative` is on for it, its AI is not paused, the site has AI\n * on, and the person still holds `ai.generate` on the site.\n *\n * ## Monday: one job per site\n *\n * On a Monday run, for each workspace with someone who asked, the sites those\n * people reach — the busiest `AI_INSIGHT_DIGEST_MAX_SITES` by yesterday's page\n * views — each get one `insight` job with the `digest` surface, created by the\n * first of them who may generate there. The job runs on the AI jobs beat like\n * any other: it reserves, meters and records its credits, and a workspace at\n * its band — a Free one that has used its credits — has its job parked by the\n * reservation like any other. `orgs/{orgId}/aiInsightDigests/{week}` records\n * the jobs; `aiInsightDigestQueue/{week}` lists the workspaces still owed a\n * delivery, so a run that delivers reads one document rather than every\n * workspace.\n *\n * ## Every run: deliver what is written\n *\n * A digest whose job is done and kept at least one insight is delivered once:\n * a `content.insightsDigest` notification (unless the person muted the\n * category) and an email through the platform's own sending path, to each\n * person who asked for the workspace's insights and reaches the site. A job\n * that failed, kept nothing, or parked for credits is SKIPPED SILENTLY: a\n * parked job is canceled rather than left to spend later, and nobody is told,\n * because nobody asked about that week in particular. A job still running\n * past `AI_INSIGHT_DIGEST_DEADLINE_MS` is canceled and skipped the same way.\n *\n * The email says only what the answer says: the insights, each traced to the\n * figures it cites, and a link to the site's Analytics page. It is sent by the\n * platform to a person who asked for it, never to a contact.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const AI_INSIGHTS_DIGEST_CRON_ID = 'ai-insights-digest'\nexport const AI_INSIGHTS_DIGEST_PATH = 'admin/ai-insights-digest'\n\n/** Orgs one call reads; a chunk that times out never advances the cursor, so the safe size is one that finishes. */\nexport const AI_INSIGHT_DIGEST_ORG_CHUNK = 25\n\n/** Sites one workspace's digest covers a week. */\nexport const AI_INSIGHT_DIGEST_MAX_SITES = 5\n\n/** Sites a workspace's sweep reads before it ranks them. */\nexport const AI_INSIGHT_DIGEST_HOST_READ_LIMIT = 50\n\n/** Workspaces one run delivers to, per week. */\nexport const AI_INSIGHT_DIGEST_DELIVER_LIMIT = 100\n\n/** How long a digest's job may take before its week is given up. */\nexport const AI_INSIGHT_DIGEST_DEADLINE_MS = 48 * 60 * 60_000\n\n/**\n * The time one call works before it stops and leaves the rest to the next:\n * inside the plugin dispatcher's 60 s function ceiling, with room for the\n * reads before the first check and the reply after the last. A call killed\n * mid-send could send one person a site's digest twice, because a send is\n * stamped after it goes.\n */\nexport const AI_INSIGHT_DIGEST_RUN_BUDGET_MS = 45_000\n\nexport const AI_INSIGHT_DIGEST_MARKERS = 'aiInsightDigests'\nexport const AI_INSIGHT_DIGEST_QUEUE = 'aiInsightDigestQueue'\n\nexport type AiInsightDigestSiteState = 'pending' | 'delivered' | 'skipped'\n\nexport interface AiInsightDigestSite {\n jobId: string\n name: string\n state: AiInsightDigestSiteState\n /**\n * The people already sent this site's digest, so a run the send rate\n * stopped never sends one twice. Dropped when the site settles, so a\n * settled marker names no person.\n */\n sent?: Record<string, boolean>\n}\n\nexport interface AiInsightDigestMarker {\n week: string\n createdAtMs: number\n sites: Record<string, AiInsightDigestSite>\n}\n\n/** What a send answered: whether it went, and whether the platform's send rate refused it for now. */\nexport interface AiInsightDigestSendResult {\n sent: boolean\n rateLimited: boolean\n}\n\n/** Everything the sweep reaches beyond Firestore, so a spec can hold each one. */\nexport interface AiInsightDigestDeps {\n firestore: Firestore\n now: Date\n flagValue: ReleaseFlagValue\n listMembers: (orgId: string) => Promise<AglynOrgMember[]>\n mayGenerate: (orgId: string, hostId: string, member: AglynOrgMember) => Promise<boolean>\n paused: (orgId: string) => Promise<boolean>\n notify: (uid: string, payload: { title: string; body: string; link: string; orgId: string; hostId: string }) => Promise<void>\n send: (email: {\n to: string\n subject: string\n text: string\n fromName: string\n /** The site the digest is about, and the org whose brand it wears (AGL-3367). */\n siteName: string\n org: Record<string, unknown>\n }) => Promise<AiInsightDigestSendResult>\n /** The console's absolute origin for an email link, or `''` when none is configured. */\n consoleOrigin: string\n /** The call's working time; `AI_INSIGHT_DIGEST_RUN_BUDGET_MS` unless a spec holds it. */\n budgetMs?: number\n}\n\nexport interface AiInsightDigestReport {\n week: string\n swept: number\n created: number\n delivered: number\n skipped: number\n /** The send rate refused or the call's time ran out; the run stops, and the next one delivers the rest. */\n deferred: boolean\n nextCursor: string | null\n done: boolean\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The week a Monday digest covers: the week the run falls in. */\nexport function aiInsightDigestWeek(now: Date): string {\n return aiInsightIsoWeek(now)\n}\n\n/** The week before, whose late digests a run still delivers. */\nfunction previousWeek(now: Date): string {\n return aiInsightIsoWeek(new Date(now.getTime() - 7 * 86_400_000))\n}\n\n/**\n * A digest email's words: the insights, the site's Analytics page, and where\n * to turn it off. The digest is an AI job metered against the workspace, so\n * it says what it drew, and it names the workspace a member of several asked\n * for it in.\n */\nexport function aiInsightDigestEmailText(input: {\n siteName: string\n productName: string\n insights: ReadonlyArray<{ text: string }>\n /** Absolute links, or `null` where no console origin is configured to build one from. */\n analyticsUrl: string | null\n settingsUrl: string | null\n /** The workspace the digest was asked for in; `''` falls back to \"this workspace\". */\n workspaceName?: string\n /** The AI credits the digest's job drew; 0 or absent says nothing. */\n creditsSpent?: number\n}): string {\n const workspace = input.workspaceName?.trim() || ''\n const credits = Math.max(0, Math.round(Number(input.creditsSpent) || 0))\n const drew = credits\n ? ` Writing it used ${credits.toLocaleString('en-US')} of ` +\n `${workspace ? `${workspace}’s` : 'the workspace’s'} AI credits.`\n : ''\n const forWhom = workspace || 'this workspace'\n return [\n `Here is what ${input.siteName}'s figures showed this week.${drew}`,\n '',\n ...input.insights.map((insight, index) => `${index + 1}. ${insight.text}`),\n '',\n input.analyticsUrl\n ? `Each of these is traced to the figures it cites. See them in ${input.productName}: ${input.analyticsUrl}`\n : `Each of these is traced to the figures it cites. See them on the site's Analytics page in ${input.productName}.`,\n '',\n input.settingsUrl\n ? `You asked for weekly insights for ${forWhom}. Turn them off in Notifications: ${input.settingsUrl}`\n : `You asked for weekly insights for ${forWhom}. Turn them off in your Notifications settings.`,\n ].join('\\n')\n}\n\nasync function readUsers(firestore: Firestore, uids: readonly string[]) {\n if (!uids.length) return new Map<string, FirebaseFirestore.DocumentSnapshot>()\n const docs = await firestore.getAll(...uids.map((uid) => firestore.collection('users').doc(uid)))\n return new Map(docs.map((doc) => [doc.id, doc]))\n}\n\n/** Creates one workspace's digests for the week; answers how many jobs it created. */\nasync function createOrgDigest(\n deps: AiInsightDigestDeps,\n orgDoc: FirebaseFirestore.QueryDocumentSnapshot,\n week: string,\n): Promise<number> {\n const { firestore, now } = deps\n const orgId = orgDoc.id\n const org = (orgDoc.data() ?? {}) as Record<string, unknown>\n if (!checkEntitlement(org as never, 'aiGenerative')) return 0\n const flagOn = isReleaseFlagOnForOrg(\n 'release_ai_generative',\n deps.flagValue,\n orgId,\n parseOrgReleaseFlagOverrides(org['releaseFlags']),\n resolveEffectivePlan(org as never),\n )\n if (!flagOn || (await deps.paused(orgId))) return 0\n const markerRef = orgDoc.ref.collection(AI_INSIGHT_DIGEST_MARKERS).doc(week)\n if ((await markerRef.get()).exists) return 0\n\n const members = (await deps.listMembers(orgId)).filter((member) => member.orgSuspended !== true)\n const users = await readUsers(firestore, members.map((member) => member.$id).sort())\n const subscribers = members\n .filter((member) => insightDigestSubscribed(users.get(member.$id)?.get(INSIGHT_DIGESTS_FIELD), orgId))\n .sort((a, b) => a.$id.localeCompare(b.$id))\n if (!subscribers.length) return 0\n\n const hosts = await firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .limit(AI_INSIGHT_DIGEST_HOST_READ_LIMIT)\n .get()\n const yesterday = new Date(now.getTime() - 86_400_000).toISOString().slice(0, 10)\n const candidates: Array<{ hostId: string; name: string; creator: string; views: number }> = []\n for (const host of hosts.docs) {\n const data = host.data() ?? {}\n if (!isAiOnForSite(org, data)) continue\n let creator: string | null = null\n for (const member of subscribers) {\n if (hostRoleFor(member, host.id) !== null && (await deps.mayGenerate(orgId, host.id, member))) {\n creator = member.$id\n break\n }\n }\n if (!creator) continue\n const day = await host.ref.collection('analytics').doc(yesterday).get()\n candidates.push({\n hostId: host.id,\n name: str(data['displayName']) || str(data['name']) || str(data['subdomain']) || host.id,\n creator,\n views: Number(day.get('total') ?? 0) || 0,\n })\n }\n const chosen = candidates\n .sort((a, b) => b.views - a.views || a.hostId.localeCompare(b.hostId))\n .slice(0, AI_INSIGHT_DIGEST_MAX_SITES)\n if (!chosen.length) return 0\n\n const sites: Record<string, AiInsightDigestSite> = {}\n for (const site of chosen) {\n const job = await createAiJob(\n firestore,\n {\n orgId,\n hostId: site.hostId,\n kind: 'insight',\n brief: `Weekly insights for ${site.name}`,\n inputs: { surface: 'digest', week },\n createdBy: site.creator,\n },\n now,\n )\n sites[site.hostId] = { jobId: job.$id, name: site.name, state: 'pending' }\n }\n const marker: AiInsightDigestMarker = { week, createdAtMs: now.getTime(), sites }\n await markerRef.set(marker)\n await firestore\n .collection(AI_INSIGHT_DIGEST_QUEUE)\n .doc(week)\n .set({ week, pending: { [orgId]: true } }, { merge: true })\n return chosen.length\n}\n\n/** Delivers or settles one workspace's digests for a week; `deferred` when the send rate refused. */\nasync function deliverOrgDigest(\n deps: AiInsightDigestDeps,\n orgId: string,\n week: string,\n overBudget: () => boolean,\n): Promise<{ delivered: number; skipped: number; settled: boolean; deferred: boolean }> {\n const { firestore, now } = deps\n const orgRef = firestore.collection('orgs').doc(orgId)\n const markerRef = orgRef.collection(AI_INSIGHT_DIGEST_MARKERS).doc(week)\n const markerSnapshot = await markerRef.get()\n const result = { delivered: 0, skipped: 0, settled: true, deferred: false }\n if (!markerSnapshot.exists) return result\n const marker = markerSnapshot.data() as AiInsightDigestMarker\n const org = ((await orgRef.get()).data() ?? {}) as Record<string, unknown>\n let members: AglynOrgMember[] | null = null\n const branding = resolveBrandingProfile(org as never)\n\n for (const [hostId, site] of Object.entries(marker.sites ?? {})) {\n if (site.state !== 'pending') continue\n const settle = async (state: AiInsightDigestSiteState) => {\n // Replaced whole rather than merged, which drops `sent`.\n const settled: AiInsightDigestSite = { jobId: site.jobId, name: site.name, state }\n await markerRef.update({ [`sites.${hostId}`]: settled })\n if (state === 'delivered') result.delivered += 1\n else result.skipped += 1\n }\n const job = await getAiJob(firestore, orgId, site.jobId)\n if (!job || job.status === 'failed' || job.status === 'canceled') {\n await settle('skipped')\n continue\n }\n if (job.status !== 'done') {\n const late = now.getTime() - Number(marker.createdAtMs ?? 0) > AI_INSIGHT_DIGEST_DEADLINE_MS\n // A job parked for credits — a Free workspace at its band — is given up\n // rather than left to spend on a week that is already over.\n if (job.status === 'needs_input' || late) {\n await cancelAiJob(firestore, orgId, site.jobId, now)\n await settle('skipped')\n } else {\n result.settled = false\n }\n continue\n }\n const answer = await orgRef.collection(AI_INSIGHTS_COLLECTION).doc(site.jobId).get()\n const record = answer.exists ? (answer.data() as AiInsightRecord) : null\n if (!record?.insights?.length) {\n await settle('skipped')\n continue\n }\n\n members ??= (await deps.listMembers(orgId)).filter((member) => member.orgSuspended !== true)\n const reaching = members.filter((member) => hostRoleFor(member, hostId) !== null)\n const users = await readUsers(firestore, reaching.map((member) => member.$id).sort())\n const host = (await firestore.collection('hosts').doc(hostId).get()).data() ?? {}\n const orgSlug = str(org['slug'])\n const subdomain = str(host['subdomain'])\n const origin = deps.consoleOrigin\n const analyticsUrl = origin && orgSlug && subdomain ? `${origin}/${orgSlug}/hosts/${subdomain}/analytics` : null\n for (const member of reaching.sort((a, b) => a.$id.localeCompare(b.$id))) {\n const user = users.get(member.$id)\n if (!insightDigestSubscribed(user?.get(INSIGHT_DIGESTS_FIELD), orgId)) continue\n if (site.sent?.[member.$id]) continue\n if (overBudget()) {\n // Out of time: what was sent is stamped, and the next run sends the rest.\n result.deferred = true\n result.settled = false\n return result\n }\n const address = str(member.email ?? user?.get('email')).toLowerCase()\n if (address.includes('@')) {\n const sent = await deps.send({\n to: address,\n subject: `Your weekly insights for ${site.name}`,\n text: aiInsightDigestEmailText({\n siteName: site.name,\n productName: branding.productName,\n insights: record.insights,\n analyticsUrl,\n settingsUrl: origin ? `${origin}/manage/notifications` : null,\n workspaceName: str(org['name']),\n creditsSpent: job.creditsSpent,\n }),\n fromName: branding.fromName,\n siteName: site.name,\n org,\n })\n if (sent.rateLimited) {\n // Nothing is marked, so the next run delivers this site whole.\n result.deferred = true\n result.settled = false\n return result\n }\n }\n if (!notificationMuted(user?.get('notificationPrefs'), 'content.insightsDigest')) {\n await deps.notify(member.$id, {\n title: `Weekly insights · ${site.name}`,\n body: record.insights[0].text,\n link: `/${hostId}/analytics`,\n orgId,\n hostId,\n })\n }\n await markerRef.set({ sites: { [hostId]: { sent: { [member.$id]: true } } } }, { merge: true })\n }\n await settle('delivered')\n }\n return result\n}\n\n/**\n * One call of the sweep: delivery for this week and the last on the first\n * call, then — on a Monday — one chunk of workspaces' creation, resumed by\n * `cursor`. The response shape is the console crons' (`nextCursor`, `done`),\n * so the scheduler's loop carries a platform of any size across calls.\n */\nexport async function runAiInsightDigestSweep(\n deps: AiInsightDigestDeps,\n options: { cursor: string | null },\n): Promise<AiInsightDigestReport> {\n const { firestore, now } = deps\n const startedAtMs = Date.now()\n const budgetMs = deps.budgetMs ?? AI_INSIGHT_DIGEST_RUN_BUDGET_MS\n const overBudget = () => Date.now() - startedAtMs > budgetMs\n const week = aiInsightDigestWeek(now)\n const report: AiInsightDigestReport = {\n week,\n swept: 0,\n created: 0,\n delivered: 0,\n skipped: 0,\n deferred: false,\n nextCursor: null,\n done: true,\n }\n\n if (!options.cursor) {\n for (const owed of [previousWeek(now), week]) {\n const queueRef = firestore.collection(AI_INSIGHT_DIGEST_QUEUE).doc(owed)\n const pending = Object.entries(((await queueRef.get()).get('pending') ?? {}) as Record<string, unknown>)\n .filter(([, owed]) => owed === true)\n .map(([orgId]) => orgId)\n .sort()\n .slice(0, AI_INSIGHT_DIGEST_DELIVER_LIMIT)\n for (const orgId of pending) {\n if (overBudget()) {\n report.deferred = true\n return report\n }\n try {\n const delivered = await deliverOrgDigest(deps, orgId, owed, overBudget)\n report.delivered += delivered.delivered\n report.skipped += delivered.skipped\n if (delivered.settled) {\n await queueRef.set({ pending: { [orgId]: false } }, { merge: true })\n }\n if (delivered.deferred) {\n report.deferred = true\n return report\n }\n } catch (error) {\n console.error('ai insight digest delivery failed', { orgId, week: owed, error })\n }\n }\n }\n }\n\n // Monday is the week's first day by ISO 8601, and the day a digest is made.\n if (now.getUTCDay() !== 1) return report\n\n let query = firestore.collection('orgs').orderBy(FieldPath.documentId()).limit(AI_INSIGHT_DIGEST_ORG_CHUNK + 1)\n if (options.cursor) query = query.startAfter(firestore.collection('orgs').doc(options.cursor))\n const page = await query.get()\n const orgs = page.docs.slice(0, AI_INSIGHT_DIGEST_ORG_CHUNK)\n for (const [index, orgDoc] of orgs.entries()) {\n if (overBudget()) {\n // Out of time: the next call resumes after the last workspace read. A\n // call that read none has no cursor to hand on, and the next scheduled\n // run starts the week's workspaces again, which the markers make safe.\n report.nextCursor = index ? orgs[index - 1].id : (options.cursor ?? null)\n report.done = report.nextCursor === null\n return report\n }\n report.swept += 1\n try {\n report.created += await createOrgDigest(deps, orgDoc, week)\n } catch (error) {\n console.error('ai insight digest creation failed', { orgId: orgDoc.id, week, error })\n }\n }\n const more = page.docs.length > AI_INSIGHT_DIGEST_ORG_CHUNK\n report.nextCursor = more ? (orgs[orgs.length - 1]?.id ?? null) : null\n report.done = !more\n return report\n}\n"],"names":["FieldPath","INSIGHT_DIGESTS_FIELD","insightDigestSubscribed","notificationMuted","hostRoleFor","checkEntitlement","resolveBrandingProfile","resolveEffectivePlan","isReleaseFlagOnForOrg","parseOrgReleaseFlagOverrides","AI_INSIGHTS_COLLECTION","aiInsightIsoWeek","isAiOnForSite","cancelAiJob","createAiJob","getAiJob","AI_INSIGHTS_DIGEST_CRON_ID","AI_INSIGHTS_DIGEST_PATH","AI_INSIGHT_DIGEST_ORG_CHUNK","AI_INSIGHT_DIGEST_MAX_SITES","AI_INSIGHT_DIGEST_HOST_READ_LIMIT","AI_INSIGHT_DIGEST_DELIVER_LIMIT","AI_INSIGHT_DIGEST_DEADLINE_MS","AI_INSIGHT_DIGEST_RUN_BUDGET_MS","AI_INSIGHT_DIGEST_MARKERS","AI_INSIGHT_DIGEST_QUEUE","str","value","trim","aiInsightDigestWeek","now","previousWeek","Date","getTime","aiInsightDigestEmailText","input","workspace","workspaceName","credits","Math","max","round","Number","creditsSpent","drew","toLocaleString","forWhom","siteName","insights","map","insight","index","text","analyticsUrl","productName","settingsUrl","join","readUsers","firestore","uids","length","Map","docs","getAll","uid","collection","doc","id","createOrgDigest","deps","orgDoc","week","orgId","org","data","flagOn","flagValue","paused","markerRef","ref","get","exists","members","listMembers","filter","member","orgSuspended","users","$id","sort","subscribers","a","b","localeCompare","hosts","where","limit","yesterday","toISOString","slice","candidates","host","day","creator","mayGenerate","push","hostId","name","views","chosen","sites","site","job","kind","brief","inputs","surface","createdBy","jobId","state","marker","createdAtMs","set","pending","merge","deliverOrgDigest","overBudget","orgRef","markerSnapshot","result","delivered","skipped","settled","deferred","branding","Object","entries","record","settle","update","status","late","answer","reaching","orgSlug","subdomain","origin","consoleOrigin","user","sent","address","email","toLowerCase","includes","send","to","subject","fromName","rateLimited","notify","title","body","link","runAiInsightDigestSweep","options","orgs","startedAtMs","budgetMs","report","swept","created","nextCursor","done","cursor","owed","queueRef","error","console","getUTCDay","query","orderBy","documentId","startAfter","page","more"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-digest.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 { FieldPath } from 'firebase-admin/firestore'\nimport { INSIGHT_DIGESTS_FIELD, insightDigestSubscribed, notificationMuted } from '@aglyn/aglyn/app-utils/notifications'\nimport { hostRoleFor } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, resolveBrandingProfile, resolveEffectivePlan } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport { isReleaseFlagOnForOrg, parseOrgReleaseFlagOverrides, type ReleaseFlagValue } from '@aglyn/aglyn/app-utils/release-flags'\nimport type { AglynOrgMember } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport { weeklySummaryPeriod } from '@aglyn/aglyn/app-utils/weekly-summary'\nimport {\n aiInsightDigestPeriod,\n AI_INSIGHTS_COLLECTION,\n aiInsightIsoWeek,\n type AiInsightRecord,\n} from '../model/ai-insight'\nimport { isAiOnForSite } from '../model/ai-site-switch'\nimport { cancelAiJob, createAiJob, getAiJob } from '../jobs/ai-jobs'\n\n/**\n * THE WEEKLY INSIGHTS (AGL-2915): `POST /api/admin/ai-insights-digest`, twice\n * a day on the console's scheduler.\n *\n * ## Who asked for it\n *\n * A person turns weekly insights on for a workspace beside the answers\n * themselves (`users/{uid}.insightDigests.{orgId}`), and off there or in\n * Notifications. It is opt-in, because each digest is a generation the\n * workspace pays for. At the sweep, every rung is asked again rather than\n * trusted from the switch: the workspace's plan includes `aiGenerative`,\n * `release_ai_generative` is on for it, its AI is not paused, the site has AI\n * on, and the person still holds `ai.generate` on the site.\n *\n * ## Monday: one job per site\n *\n * On a Monday run, for each workspace with someone who asked, the sites those\n * people reach — the busiest `AI_INSIGHT_DIGEST_MAX_SITES` by yesterday's page\n * views — each get one `insight` job with the `digest` surface, created by the\n * first of them who may generate there. The job runs on the AI jobs beat like\n * any other: it reserves, meters and records its credits, and a workspace at\n * its band — a Free one that has used its credits — has its job parked by the\n * reservation like any other. `orgs/{orgId}/aiInsightDigests/{week}` records\n * the jobs; `aiInsightDigestQueue/{week}` lists the workspaces still owed a\n * delivery, so a run that delivers reads one document rather than every\n * workspace.\n *\n * ## Every run: deliver what is written\n *\n * A digest whose job is done and kept at least one insight is delivered once:\n * a `content.insightsDigest` notification (unless the person muted the\n * category) and an email through the platform's own sending path, to each\n * person who asked for the workspace's insights and reaches the site. A job\n * that failed, kept nothing, or parked for credits is SKIPPED SILENTLY: a\n * parked job is canceled rather than left to spend later, and nobody is told,\n * because nobody asked about that week in particular. A job still running\n * past `AI_INSIGHT_DIGEST_DEADLINE_MS` is canceled and skipped the same way.\n *\n * The email says only what the answer says: the insights, each traced to the\n * figures it cites, and a link to the site's Analytics page. It is sent by the\n * platform to a person who asked for it, never to a contact.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const AI_INSIGHTS_DIGEST_CRON_ID = 'ai-insights-digest'\nexport const AI_INSIGHTS_DIGEST_PATH = 'admin/ai-insights-digest'\n\n/** Orgs one call reads; a chunk that times out never advances the cursor, so the safe size is one that finishes. */\nexport const AI_INSIGHT_DIGEST_ORG_CHUNK = 25\n\n/** Sites one workspace's digest covers a week. */\nexport const AI_INSIGHT_DIGEST_MAX_SITES = 5\n\n/** Sites a workspace's sweep reads before it ranks them. */\nexport const AI_INSIGHT_DIGEST_HOST_READ_LIMIT = 50\n\n/** Workspaces one run delivers to, per week. */\nexport const AI_INSIGHT_DIGEST_DELIVER_LIMIT = 100\n\n/** How long a digest's job may take before its week is given up. */\nexport const AI_INSIGHT_DIGEST_DEADLINE_MS = 48 * 60 * 60_000\n\n/**\n * The time one call works before it stops and leaves the rest to the next:\n * inside the plugin dispatcher's 60 s function ceiling, with room for the\n * reads before the first check and the reply after the last. A call killed\n * mid-send could send one person a site's digest twice, because a send is\n * stamped after it goes.\n */\nexport const AI_INSIGHT_DIGEST_RUN_BUDGET_MS = 45_000\n\nexport const AI_INSIGHT_DIGEST_MARKERS = 'aiInsightDigests'\nexport const AI_INSIGHT_DIGEST_QUEUE = 'aiInsightDigestQueue'\n\nexport type AiInsightDigestSiteState = 'pending' | 'delivered' | 'skipped'\n\nexport interface AiInsightDigestSite {\n jobId: string\n name: string\n state: AiInsightDigestSiteState\n /**\n * The people already sent this site's digest, so a run the send rate\n * stopped never sends one twice. Dropped when the site settles, so a\n * settled marker names no person.\n */\n sent?: Record<string, boolean>\n}\n\nexport interface AiInsightDigestMarker {\n week: string\n createdAtMs: number\n sites: Record<string, AiInsightDigestSite>\n}\n\n/** What a send answered: whether it went, and whether the platform's send rate refused it for now. */\nexport interface AiInsightDigestSendResult {\n sent: boolean\n rateLimited: boolean\n}\n\n/** Everything the sweep reaches beyond Firestore, so a spec can hold each one. */\nexport interface AiInsightDigestDeps {\n firestore: Firestore\n now: Date\n flagValue: ReleaseFlagValue\n listMembers: (orgId: string) => Promise<AglynOrgMember[]>\n mayGenerate: (orgId: string, hostId: string, member: AglynOrgMember) => Promise<boolean>\n paused: (orgId: string) => Promise<boolean>\n notify: (uid: string, payload: { title: string; body: string; link: string; orgId: string; hostId: string }) => Promise<void>\n send: (email: {\n to: string\n subject: string\n text: string\n fromName: string\n /** The site the digest is about, and the org whose brand it wears (AGL-3367). */\n siteName: string\n org: Record<string, unknown>\n }) => Promise<AiInsightDigestSendResult>\n /** The console's absolute origin for an email link, or `''` when none is configured. */\n consoleOrigin: string\n /** The call's working time; `AI_INSIGHT_DIGEST_RUN_BUDGET_MS` unless a spec holds it. */\n budgetMs?: number\n}\n\nexport interface AiInsightDigestReport {\n week: string\n swept: number\n created: number\n delivered: number\n skipped: number\n /** The send rate refused or the call's time ran out; the run stops, and the next one delivers the rest. */\n deferred: boolean\n nextCursor: string | null\n done: boolean\n}\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** The week a Monday digest covers: the week the run falls in. */\nexport function aiInsightDigestWeek(now: Date): string {\n return aiInsightIsoWeek(now)\n}\n\n/** The week before, whose late digests a run still delivers. */\nfunction previousWeek(now: Date): string {\n return aiInsightIsoWeek(new Date(now.getTime() - 7 * 86_400_000))\n}\n\n/**\n * A digest email's words: the insights, the site's Analytics page, and where\n * to turn it off. The digest is an AI job metered against the workspace, so\n * it says what it drew, and it names the workspace a member of several asked\n * for it in.\n */\nexport function aiInsightDigestEmailText(input: {\n siteName: string\n productName: string\n insights: ReadonlyArray<{ text: string }>\n /** Absolute links, or `null` where no console origin is configured to build one from. */\n analyticsUrl: string | null\n settingsUrl: string | null\n /** The workspace the digest was asked for in; `''` falls back to \"this workspace\". */\n workspaceName?: string\n /** The AI credits the digest's job drew; 0 or absent says nothing. */\n creditsSpent?: number\n /** The week the figures cover, \"Oct 5 – Oct 11\"; absent says \"this week\". */\n periodLabel?: string\n}): string {\n const workspace = input.workspaceName?.trim() || ''\n const credits = Math.max(0, Math.round(Number(input.creditsSpent) || 0))\n const drew = credits\n ? ` Writing it used ${credits.toLocaleString('en-US')} of ` +\n `${workspace ? `${workspace}’s` : 'the workspace’s'} AI credits.`\n : ''\n const forWhom = workspace || 'this workspace'\n return [\n input.periodLabel\n ? `Here is what ${input.siteName}'s figures showed last week, ${input.periodLabel}.${drew}`\n : `Here is what ${input.siteName}'s figures showed this week.${drew}`,\n '',\n ...input.insights.map((insight, index) => `${index + 1}. ${insight.text}`),\n '',\n input.analyticsUrl\n ? `Each of these is traced to the figures it cites. See them in ${input.productName}: ${input.analyticsUrl}`\n : `Each of these is traced to the figures it cites. See them on the site's Analytics page in ${input.productName}.`,\n '',\n input.settingsUrl\n ? `You asked for weekly insights for ${forWhom}. Turn them off in Notifications: ${input.settingsUrl}`\n : `You asked for weekly insights for ${forWhom}. Turn them off in your Notifications settings.`,\n ].join('\\n')\n}\n\nasync function readUsers(firestore: Firestore, uids: readonly string[]) {\n if (!uids.length) return new Map<string, FirebaseFirestore.DocumentSnapshot>()\n const docs = await firestore.getAll(...uids.map((uid) => firestore.collection('users').doc(uid)))\n return new Map(docs.map((doc) => [doc.id, doc]))\n}\n\n/** Creates one workspace's digests for the week; answers how many jobs it created. */\nasync function createOrgDigest(\n deps: AiInsightDigestDeps,\n orgDoc: FirebaseFirestore.QueryDocumentSnapshot,\n week: string,\n): Promise<number> {\n const { firestore, now } = deps\n const orgId = orgDoc.id\n const org = (orgDoc.data() ?? {}) as Record<string, unknown>\n if (!checkEntitlement(org as never, 'aiGenerative')) return 0\n const flagOn = isReleaseFlagOnForOrg(\n 'release_ai_generative',\n deps.flagValue,\n orgId,\n parseOrgReleaseFlagOverrides(org['releaseFlags']),\n resolveEffectivePlan(org as never),\n )\n if (!flagOn || (await deps.paused(orgId))) return 0\n const markerRef = orgDoc.ref.collection(AI_INSIGHT_DIGEST_MARKERS).doc(week)\n if ((await markerRef.get()).exists) return 0\n\n const members = (await deps.listMembers(orgId)).filter((member) => member.orgSuspended !== true)\n const users = await readUsers(firestore, members.map((member) => member.$id).sort())\n const subscribers = members\n .filter((member) => insightDigestSubscribed(users.get(member.$id)?.get(INSIGHT_DIGESTS_FIELD), orgId))\n .sort((a, b) => a.$id.localeCompare(b.$id))\n if (!subscribers.length) return 0\n\n const hosts = await firestore\n .collection('hosts')\n .where('orgId', '==', orgId)\n .limit(AI_INSIGHT_DIGEST_HOST_READ_LIMIT)\n .get()\n const yesterday = new Date(now.getTime() - 86_400_000).toISOString().slice(0, 10)\n const candidates: Array<{ hostId: string; name: string; creator: string; views: number }> = []\n for (const host of hosts.docs) {\n const data = host.data() ?? {}\n if (!isAiOnForSite(org, data)) continue\n let creator: string | null = null\n for (const member of subscribers) {\n if (hostRoleFor(member, host.id) !== null && (await deps.mayGenerate(orgId, host.id, member))) {\n creator = member.$id\n break\n }\n }\n if (!creator) continue\n const day = await host.ref.collection('analytics').doc(yesterday).get()\n candidates.push({\n hostId: host.id,\n name: str(data['displayName']) || str(data['name']) || str(data['subdomain']) || host.id,\n creator,\n views: Number(day.get('total') ?? 0) || 0,\n })\n }\n const chosen = candidates\n .sort((a, b) => b.views - a.views || a.hostId.localeCompare(b.hostId))\n .slice(0, AI_INSIGHT_DIGEST_MAX_SITES)\n if (!chosen.length) return 0\n\n const sites: Record<string, AiInsightDigestSite> = {}\n for (const site of chosen) {\n const job = await createAiJob(\n firestore,\n {\n orgId,\n hostId: site.hostId,\n kind: 'insight',\n brief: `Weekly insights for ${site.name}`,\n inputs: { surface: 'digest', week, through: weeklySummaryPeriod(now).to },\n createdBy: site.creator,\n },\n now,\n )\n sites[site.hostId] = { jobId: job.$id, name: site.name, state: 'pending' }\n }\n const marker: AiInsightDigestMarker = { week, createdAtMs: now.getTime(), sites }\n await markerRef.set(marker)\n await firestore\n .collection(AI_INSIGHT_DIGEST_QUEUE)\n .doc(week)\n .set({ week, pending: { [orgId]: true } }, { merge: true })\n return chosen.length\n}\n\n/** Delivers or settles one workspace's digests for a week; `deferred` when the send rate refused. */\nasync function deliverOrgDigest(\n deps: AiInsightDigestDeps,\n orgId: string,\n week: string,\n overBudget: () => boolean,\n): Promise<{ delivered: number; skipped: number; settled: boolean; deferred: boolean }> {\n const { firestore, now } = deps\n const orgRef = firestore.collection('orgs').doc(orgId)\n const markerRef = orgRef.collection(AI_INSIGHT_DIGEST_MARKERS).doc(week)\n const markerSnapshot = await markerRef.get()\n const result = { delivered: 0, skipped: 0, settled: true, deferred: false }\n if (!markerSnapshot.exists) return result\n const marker = markerSnapshot.data() as AiInsightDigestMarker\n const org = ((await orgRef.get()).data() ?? {}) as Record<string, unknown>\n let members: AglynOrgMember[] | null = null\n const branding = resolveBrandingProfile(org as never)\n\n for (const [hostId, site] of Object.entries(marker.sites ?? {})) {\n if (site.state !== 'pending') continue\n const settle = async (state: AiInsightDigestSiteState) => {\n // Replaced whole rather than merged, which drops `sent`.\n const settled: AiInsightDigestSite = { jobId: site.jobId, name: site.name, state }\n await markerRef.update({ [`sites.${hostId}`]: settled })\n if (state === 'delivered') result.delivered += 1\n else result.skipped += 1\n }\n const job = await getAiJob(firestore, orgId, site.jobId)\n if (!job || job.status === 'failed' || job.status === 'canceled') {\n await settle('skipped')\n continue\n }\n if (job.status !== 'done') {\n const late = now.getTime() - Number(marker.createdAtMs ?? 0) > AI_INSIGHT_DIGEST_DEADLINE_MS\n // A job parked for credits — a Free workspace at its band — is given up\n // rather than left to spend on a week that is already over.\n if (job.status === 'needs_input' || late) {\n await cancelAiJob(firestore, orgId, site.jobId, now)\n await settle('skipped')\n } else {\n result.settled = false\n }\n continue\n }\n const answer = await orgRef.collection(AI_INSIGHTS_COLLECTION).doc(site.jobId).get()\n const record = answer.exists ? (answer.data() as AiInsightRecord) : null\n if (!record?.insights?.length) {\n await settle('skipped')\n continue\n }\n\n members ??= (await deps.listMembers(orgId)).filter((member) => member.orgSuspended !== true)\n const reaching = members.filter((member) => hostRoleFor(member, hostId) !== null)\n const users = await readUsers(firestore, reaching.map((member) => member.$id).sort())\n const host = (await firestore.collection('hosts').doc(hostId).get()).data() ?? {}\n const orgSlug = str(org['slug'])\n const subdomain = str(host['subdomain'])\n const origin = deps.consoleOrigin\n const analyticsUrl = origin && orgSlug && subdomain ? `${origin}/${orgSlug}/hosts/${subdomain}/analytics` : null\n for (const member of reaching.sort((a, b) => a.$id.localeCompare(b.$id))) {\n const user = users.get(member.$id)\n if (!insightDigestSubscribed(user?.get(INSIGHT_DIGESTS_FIELD), orgId)) continue\n if (site.sent?.[member.$id]) continue\n if (overBudget()) {\n // Out of time: what was sent is stamped, and the next run sends the rest.\n result.deferred = true\n result.settled = false\n return result\n }\n const address = str(member.email ?? user?.get('email')).toLowerCase()\n if (address.includes('@')) {\n const sent = await deps.send({\n to: address,\n subject: `Your weekly insights for ${site.name}`,\n text: aiInsightDigestEmailText({\n siteName: site.name,\n productName: branding.productName,\n insights: record.insights,\n analyticsUrl,\n settingsUrl: origin ? `${origin}/manage/notifications` : null,\n workspaceName: str(org['name']),\n creditsSpent: job.creditsSpent,\n periodLabel: aiInsightDigestPeriod(job.inputs, new Date(Number(marker.createdAtMs) || now.getTime())).label,\n }),\n fromName: branding.fromName,\n siteName: site.name,\n org,\n })\n if (sent.rateLimited) {\n // Nothing is marked, so the next run delivers this site whole.\n result.deferred = true\n result.settled = false\n return result\n }\n }\n if (!notificationMuted(user?.get('notificationPrefs'), 'content.insightsDigest')) {\n await deps.notify(member.$id, {\n title: `Weekly insights · ${site.name}`,\n body: record.insights[0].text,\n link: `/${hostId}/analytics`,\n orgId,\n hostId,\n })\n }\n await markerRef.set({ sites: { [hostId]: { sent: { [member.$id]: true } } } }, { merge: true })\n }\n await settle('delivered')\n }\n return result\n}\n\n/**\n * One call of the sweep: delivery for this week and the last on the first\n * call, then — on a Monday — one chunk of workspaces' creation, resumed by\n * `cursor`. The response shape is the console crons' (`nextCursor`, `done`),\n * so the scheduler's loop carries a platform of any size across calls.\n */\nexport async function runAiInsightDigestSweep(\n deps: AiInsightDigestDeps,\n options: { cursor: string | null },\n): Promise<AiInsightDigestReport> {\n const { firestore, now } = deps\n const startedAtMs = Date.now()\n const budgetMs = deps.budgetMs ?? AI_INSIGHT_DIGEST_RUN_BUDGET_MS\n const overBudget = () => Date.now() - startedAtMs > budgetMs\n const week = aiInsightDigestWeek(now)\n const report: AiInsightDigestReport = {\n week,\n swept: 0,\n created: 0,\n delivered: 0,\n skipped: 0,\n deferred: false,\n nextCursor: null,\n done: true,\n }\n\n if (!options.cursor) {\n for (const owed of [previousWeek(now), week]) {\n const queueRef = firestore.collection(AI_INSIGHT_DIGEST_QUEUE).doc(owed)\n const pending = Object.entries(((await queueRef.get()).get('pending') ?? {}) as Record<string, unknown>)\n .filter(([, owed]) => owed === true)\n .map(([orgId]) => orgId)\n .sort()\n .slice(0, AI_INSIGHT_DIGEST_DELIVER_LIMIT)\n for (const orgId of pending) {\n if (overBudget()) {\n report.deferred = true\n return report\n }\n try {\n const delivered = await deliverOrgDigest(deps, orgId, owed, overBudget)\n report.delivered += delivered.delivered\n report.skipped += delivered.skipped\n if (delivered.settled) {\n await queueRef.set({ pending: { [orgId]: false } }, { merge: true })\n }\n if (delivered.deferred) {\n report.deferred = true\n return report\n }\n } catch (error) {\n console.error('ai insight digest delivery failed', { orgId, week: owed, error })\n }\n }\n }\n }\n\n // Monday is the week's first day by ISO 8601, and the day a digest is made.\n if (now.getUTCDay() !== 1) return report\n\n let query = firestore.collection('orgs').orderBy(FieldPath.documentId()).limit(AI_INSIGHT_DIGEST_ORG_CHUNK + 1)\n if (options.cursor) query = query.startAfter(firestore.collection('orgs').doc(options.cursor))\n const page = await query.get()\n const orgs = page.docs.slice(0, AI_INSIGHT_DIGEST_ORG_CHUNK)\n for (const [index, orgDoc] of orgs.entries()) {\n if (overBudget()) {\n // Out of time: the next call resumes after the last workspace read. A\n // call that read none has no cursor to hand on, and the next scheduled\n // run starts the week's workspaces again, which the markers make safe.\n report.nextCursor = index ? orgs[index - 1].id : (options.cursor ?? null)\n report.done = report.nextCursor === null\n return report\n }\n report.swept += 1\n try {\n report.created += await createOrgDigest(deps, orgDoc, week)\n } catch (error) {\n console.error('ai insight digest creation failed', { orgId: orgDoc.id, week, error })\n }\n }\n const more = page.docs.length > AI_INSIGHT_DIGEST_ORG_CHUNK\n report.nextCursor = more ? (orgs[orgs.length - 1]?.id ?? null) : null\n report.done = !more\n return report\n}\n"],"names":["FieldPath","INSIGHT_DIGESTS_FIELD","insightDigestSubscribed","notificationMuted","hostRoleFor","checkEntitlement","resolveBrandingProfile","resolveEffectivePlan","isReleaseFlagOnForOrg","parseOrgReleaseFlagOverrides","weeklySummaryPeriod","aiInsightDigestPeriod","AI_INSIGHTS_COLLECTION","aiInsightIsoWeek","isAiOnForSite","cancelAiJob","createAiJob","getAiJob","AI_INSIGHTS_DIGEST_CRON_ID","AI_INSIGHTS_DIGEST_PATH","AI_INSIGHT_DIGEST_ORG_CHUNK","AI_INSIGHT_DIGEST_MAX_SITES","AI_INSIGHT_DIGEST_HOST_READ_LIMIT","AI_INSIGHT_DIGEST_DELIVER_LIMIT","AI_INSIGHT_DIGEST_DEADLINE_MS","AI_INSIGHT_DIGEST_RUN_BUDGET_MS","AI_INSIGHT_DIGEST_MARKERS","AI_INSIGHT_DIGEST_QUEUE","str","value","trim","aiInsightDigestWeek","now","previousWeek","Date","getTime","aiInsightDigestEmailText","input","workspace","workspaceName","credits","Math","max","round","Number","creditsSpent","drew","toLocaleString","forWhom","periodLabel","siteName","insights","map","insight","index","text","analyticsUrl","productName","settingsUrl","join","readUsers","firestore","uids","length","Map","docs","getAll","uid","collection","doc","id","createOrgDigest","deps","orgDoc","week","orgId","org","data","flagOn","flagValue","paused","markerRef","ref","get","exists","members","listMembers","filter","member","orgSuspended","users","$id","sort","subscribers","a","b","localeCompare","hosts","where","limit","yesterday","toISOString","slice","candidates","host","day","creator","mayGenerate","push","hostId","name","views","chosen","sites","site","job","kind","brief","inputs","surface","through","to","createdBy","jobId","state","marker","createdAtMs","set","pending","merge","deliverOrgDigest","overBudget","orgRef","markerSnapshot","result","delivered","skipped","settled","deferred","branding","Object","entries","record","settle","update","status","late","answer","reaching","orgSlug","subdomain","origin","consoleOrigin","user","sent","address","email","toLowerCase","includes","send","subject","label","fromName","rateLimited","notify","title","body","link","runAiInsightDigestSweep","options","orgs","startedAtMs","budgetMs","report","swept","created","nextCursor","done","cursor","owed","queueRef","error","console","getUTCDay","query","orderBy","documentId","startAfter","page","more"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { type ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures';
|
|
18
18
|
import { type AiInsightSurface, type AiInsightTable } from '../model/ai-insight';
|
|
19
|
+
import { type ReleasedFigurePlugins } from '@aglyn/tenant-data-admin/server/figure-readers';
|
|
19
20
|
import type { AiInsightReadRequest } from '../tools/ai-insight-tool';
|
|
20
21
|
import type { AiDatasetCatalogEntry } from './ai-dataset-catalog';
|
|
21
22
|
/**
|
|
@@ -30,7 +31,7 @@ import type { AiDatasetCatalogEntry } from './ai-dataset-catalog';
|
|
|
30
31
|
* to the job's workspace, and the member's reach, are the job's own checks.
|
|
31
32
|
*/
|
|
32
33
|
/** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */
|
|
33
|
-
export type AiReleasedPlugins =
|
|
34
|
+
export type AiReleasedPlugins = ReleasedFigurePlugins;
|
|
34
35
|
export interface AiInsightReaderContext {
|
|
35
36
|
orgId: string;
|
|
36
37
|
/** The site the figures are about; `null` for a workspace-level question. */
|
|
@@ -14,35 +14,20 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/ import {
|
|
18
|
-
import { isHostPluginEnabled, isPluginEnabled } from "@aglyn/aglyn/plugin-manager/enabled-plugins";
|
|
19
|
-
import { listPluginFigureReaders, normalizePluginFigureTable, pluginFigureParams, pluginFigureReaderPlugin } from "@aglyn/aglyn/plugin-manager/plugin-figures";
|
|
17
|
+
*/ import { normalizePluginFigureTable, pluginFigureParams } from "@aglyn/aglyn/plugin-manager/plugin-figures";
|
|
20
18
|
import { AI_INSIGHT_MAX_READS, AI_INSIGHT_SURFACE_READERS, aiInsightCellText } from "../model/ai-insight.js";
|
|
21
|
-
import {
|
|
22
|
-
const releasedByFlags = (pluginIds, orgId)=>filterEnabledPluginsByReleaseFlags(pluginIds, {
|
|
23
|
-
orgId,
|
|
24
|
-
authorization: null
|
|
25
|
-
});
|
|
19
|
+
import { eligibleFigureReaders } from "@aglyn/tenant-data-admin/server/figure-readers";
|
|
26
20
|
/** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */ export async function aiInsightReaders(context) {
|
|
27
|
-
var _context_released;
|
|
28
21
|
const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface];
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
];
|
|
39
|
-
const released = new Set(plugins.length ? await ((_context_released = context.released) != null ? _context_released : releasedByFlags)(plugins, context.orgId) : []);
|
|
40
|
-
return candidates.filter((resolved)=>{
|
|
41
|
-
const plugin = pluginFigureReaderPlugin(resolved);
|
|
42
|
-
if (!plugin) return true;
|
|
43
|
-
if (!released.has(plugin)) return false;
|
|
44
|
-
return context.hostId ? isHostPluginEnabled(org, context.host, plugin) : isPluginEnabled(org, plugin);
|
|
45
|
-
});
|
|
22
|
+
return eligibleFigureReaders(_extends({
|
|
23
|
+
orgId: context.orgId,
|
|
24
|
+
hostId: context.hostId,
|
|
25
|
+
org: context.org,
|
|
26
|
+
host: context.host,
|
|
27
|
+
accept: (reader)=>prefixes.includes(reader.id.split('.')[0])
|
|
28
|
+
}, context.released ? {
|
|
29
|
+
released: context.released
|
|
30
|
+
} : {}));
|
|
46
31
|
}
|
|
47
32
|
/** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */ export function aiInsightReaderDays(windows, asked) {
|
|
48
33
|
if (!windows.length) return 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-readers.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 { isHostPluginEnabled, isPluginEnabled } from '@aglyn/aglyn/plugin-manager/enabled-plugins'\nimport {\n listPluginFigureReaders,\n normalizePluginFigureTable,\n pluginFigureParams,\n pluginFigureReaderPlugin,\n type ResolvedPluginFigureReader,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_MAX_READS,\n AI_INSIGHT_SURFACE_READERS,\n aiInsightCellText,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport { filterEnabledPluginsByReleaseFlags } from '@aglyn/tenant-data-admin/server/release-flags'\nimport type { AiInsightReadRequest } from '../tools/ai-insight-tool'\nimport type { AiDatasetCatalogEntry } from './ai-dataset-catalog'\n\n/**\n * Which figure readers an insight job may read, and reading them (AGL-2915).\n *\n * The caller's half of the `plugin-figures` contract: the seam carries no\n * authorization, so before a reader is offered to the model — let alone read\n * — this establishes that the surface asks about its kind of figures, that\n * the workspace's plan includes the feature the figures are sold under, and\n * that the plugin whose records they are runs here: past its release flag,\n * on for the workspace and not switched off for the site. The site belonging\n * to the job's workspace, and the member's reach, are the job's own checks.\n */\n\n/** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */\nexport type AiReleasedPlugins = (pluginIds: string[], orgId: string) => Promise<string[]>\n\nconst releasedByFlags: AiReleasedPlugins = (pluginIds, orgId) =>\n filterEnabledPluginsByReleaseFlags(pluginIds, { orgId, authorization: null })\n\nexport interface AiInsightReaderContext {\n orgId: string\n /** The site the figures are about; `null` for a workspace-level question. */\n hostId: string | null\n /** The org document the machine or the door read. */\n org: object | null\n /** The site's host document, already checked to belong to the org; `null` with no site. */\n host: Record<string, unknown> | null\n surface: AiInsightSurface\n released?: AiReleasedPlugins\n}\n\n/** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */\nexport async function aiInsightReaders(context: AiInsightReaderContext): Promise<ResolvedPluginFigureReader[]> {\n const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface]\n const org = context.org as { enabledPlugins?: string[] } | null\n const candidates = listPluginFigureReaders().filter(({ reader }) => {\n if (!prefixes.includes(reader.id.split('.')[0])) return false\n // A site reader needs a site; a workspace reader reads with or without one.\n if (reader.scope === 'site' && !context.hostId) return false\n return !reader.feature || checkEntitlement(org as never, reader.feature)\n })\n const plugins = [...new Set(candidates.map(pluginFigureReaderPlugin).filter((id): id is string => Boolean(id)))]\n const released = new Set(plugins.length ? await (context.released ?? releasedByFlags)(plugins, context.orgId) : [])\n return candidates.filter((resolved) => {\n const plugin = pluginFigureReaderPlugin(resolved)\n if (!plugin) return true\n if (!released.has(plugin)) return false\n return context.hostId\n ? isHostPluginEnabled(org, context.host as { disabledPlugins?: string[] } | null, plugin)\n : isPluginEnabled(org, plugin)\n })\n}\n\n/** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */\nexport function aiInsightReaderDays(windows: readonly number[], asked: number): number {\n if (!windows.length) return 0\n if (windows.includes(asked)) return asked\n return [...windows].sort((a, b) => Math.abs(a - asked) - Math.abs(b - asked) || a - b)[0]\n}\n\n/** The readers as the read prompt lists them: id, what each answers, its windows and its parameters. */\nexport function aiInsightReaderCatalog(readers: readonly ResolvedPluginFigureReader[]): string {\n return readers\n .map(({ reader }) => {\n const windows = reader.windows.length\n ? ` Windows: ${reader.windows.join(', ')} days.`\n : ' Current totals, with no window: days 0.'\n const params = (reader.params ?? []).length\n ? ` Parameters: ${(reader.params ?? [])\n .map((param) => `${param.name} (${param.required ? 'required' : 'optional'}) — ${param.description}`)\n .join('; ')}`\n : ''\n return `- ${reader.id} — ${reader.label}: ${reader.description}${windows}${params}`\n })\n .join('\\n')\n}\n\n/** The datasets a datasets question may name, as the read prompt lists them. */\nexport function aiInsightDatasetCatalog(datasets: readonly AiDatasetCatalogEntry[]): string {\n if (!datasets.length) return 'There are no datasets you may read here.'\n return datasets\n .map(\n (dataset) =>\n `- ${dataset.name} (id ${dataset.id}): ${dataset.fields.map((field) => `${field.name} [${field.type}]`).join(', ') || 'no fields'}`,\n )\n .join('\\n')\n}\n\nexport interface AiInsightReadResult {\n tables: AiInsightTable[]\n /** Why a request that named a real reader was not read, in its owner's words, for the log. */\n refusals: string[]\n}\n\n/**\n * The tables for the reads a job asked for: each request held to the readers\n * this job may read, its window to that reader's, its parameters to those it\n * declares; each table held to the contract by `normalizePluginFigureTable`.\n * A reader asked for twice with the same window and parameters is read once.\n */\nexport async function readAiInsightTables(\n readers: readonly ResolvedPluginFigureReader[],\n requests: readonly AiInsightReadRequest[],\n base: { orgId: string; hostId: string | null; uid: string | null; now: Date },\n maxTables: number = AI_INSIGHT_MAX_READS,\n): Promise<AiInsightReadResult> {\n const tables: AiInsightTable[] = []\n const refusals: string[] = []\n const seen = new Set<string>()\n for (const request of requests) {\n if (tables.length === maxTables) break\n const resolved = readers.find(({ reader }) => reader.id === request.reader)\n if (!resolved) continue\n const { reader } = resolved\n const days = aiInsightReaderDays(reader.windows, request.days)\n const params = pluginFigureParams(reader, request.params)\n if (params.ok === false) {\n refusals.push(`${reader.id}: missing ${params.missing}`)\n continue\n }\n const key = JSON.stringify([reader.id, days, params.params])\n if (seen.has(key)) continue\n seen.add(key)\n try {\n const read = await reader.read({ ...base, days, params: params.params })\n if (read.ok === false) {\n refusals.push(`${reader.id}: ${read.error}`)\n continue\n }\n const table = normalizePluginFigureTable(read.table)\n if (!table) {\n refusals.push(`${reader.id}: not a table`)\n continue\n }\n tables.push({ ...table, ref: `t${tables.length + 1}`, reader: reader.id, days, scope: reader.scope })\n } catch (error) {\n // One reader's fault is not the answer's: the rest are still read.\n console.error('ai insight reader failed', { reader: reader.id, orgId: base.orgId, error })\n refusals.push(`${reader.id}: failed`)\n }\n }\n return { tables, refusals }\n}\n\n/** One table as the answer prompt shows it: a heading, the columns, numbered rows, and its notes. */\nexport function aiInsightTableText(table: AiInsightTable): string {\n const period = table.period ? ` · ${table.period.from} to ${table.period.to} (${table.period.days} days)` : ''\n const lines = [\n `${table.ref} · ${table.title} (${table.source.label})${period}`,\n `Columns: ${table.columns.map((column) => column.label).join(' | ')}`,\n ...table.rows.map(\n (row, index) =>\n `${index}: ${table.columns.map((column) => aiInsightCellText(table, column.key, row[column.key])).join(' | ')}`,\n ),\n ]\n if (!table.rows.length) lines.push('(no rows)')\n if (table.omitted) lines.push(`${table.omitted} more rows were left out of this table.`)\n for (const note of table.notes) lines.push(`Note: ${note}`)\n return lines.join('\\n')\n}\n"],"names":["checkEntitlement","isHostPluginEnabled","isPluginEnabled","listPluginFigureReaders","normalizePluginFigureTable","pluginFigureParams","pluginFigureReaderPlugin","AI_INSIGHT_MAX_READS","AI_INSIGHT_SURFACE_READERS","aiInsightCellText","filterEnabledPluginsByReleaseFlags","releasedByFlags","pluginIds","orgId","authorization","aiInsightReaders","context","prefixes","surface","org","candidates","filter","reader","includes","id","split","scope","hostId","feature","plugins","Set","map","Boolean","released","length","resolved","plugin","has","host","aiInsightReaderDays","windows","asked","sort","a","b","Math","abs","aiInsightReaderCatalog","readers","join","params","param","name","required","description","label","aiInsightDatasetCatalog","datasets","dataset","fields","field","type","readAiInsightTables","requests","base","maxTables","tables","refusals","seen","request","find","days","ok","push","missing","key","JSON","stringify","add","read","error","table","ref","console","aiInsightTableText","period","from","to","lines","title","source","columns","column","rows","row","index","omitted","note","notes"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/insights/ai-insight-readers.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 normalizePluginFigureTable,\n pluginFigureParams,\n type ResolvedPluginFigureReader,\n} from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_MAX_READS,\n AI_INSIGHT_SURFACE_READERS,\n aiInsightCellText,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport {\n eligibleFigureReaders,\n type ReleasedFigurePlugins,\n} from '@aglyn/tenant-data-admin/server/figure-readers'\nimport type { AiInsightReadRequest } from '../tools/ai-insight-tool'\nimport type { AiDatasetCatalogEntry } from './ai-dataset-catalog'\n\n/**\n * Which figure readers an insight job may read, and reading them (AGL-2915).\n *\n * The caller's half of the `plugin-figures` contract: the seam carries no\n * authorization, so before a reader is offered to the model — let alone read\n * — this establishes that the surface asks about its kind of figures, that\n * the workspace's plan includes the feature the figures are sold under, and\n * that the plugin whose records they are runs here: past its release flag,\n * on for the workspace and not switched off for the site. The site belonging\n * to the job's workspace, and the member's reach, are the job's own checks.\n */\n\n/** Which of a set of plugins are past their release flags for an org; the platform's flags by default. */\nexport type AiReleasedPlugins = ReleasedFigurePlugins\n\nexport interface AiInsightReaderContext {\n orgId: string\n /** The site the figures are about; `null` for a workspace-level question. */\n hostId: string | null\n /** The org document the machine or the door read. */\n org: object | null\n /** The site's host document, already checked to belong to the org; `null` with no site. */\n host: Record<string, unknown> | null\n surface: AiInsightSurface\n released?: AiReleasedPlugins\n}\n\n/** The readers a job on this surface may read, ordered by id so a prompt lists them in the same order every time. */\nexport async function aiInsightReaders(context: AiInsightReaderContext): Promise<ResolvedPluginFigureReader[]> {\n const prefixes = AI_INSIGHT_SURFACE_READERS[context.surface]\n return eligibleFigureReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host: context.host,\n accept: (reader) => prefixes.includes(reader.id.split('.')[0]),\n ...(context.released ? { released: context.released } : {}),\n })\n}\n\n/** The window a reader is read over: the one asked for when it covers it, else the nearest it does. */\nexport function aiInsightReaderDays(windows: readonly number[], asked: number): number {\n if (!windows.length) return 0\n if (windows.includes(asked)) return asked\n return [...windows].sort((a, b) => Math.abs(a - asked) - Math.abs(b - asked) || a - b)[0]\n}\n\n/** The readers as the read prompt lists them: id, what each answers, its windows and its parameters. */\nexport function aiInsightReaderCatalog(readers: readonly ResolvedPluginFigureReader[]): string {\n return readers\n .map(({ reader }) => {\n const windows = reader.windows.length\n ? ` Windows: ${reader.windows.join(', ')} days.`\n : ' Current totals, with no window: days 0.'\n const params = (reader.params ?? []).length\n ? ` Parameters: ${(reader.params ?? [])\n .map((param) => `${param.name} (${param.required ? 'required' : 'optional'}) — ${param.description}`)\n .join('; ')}`\n : ''\n return `- ${reader.id} — ${reader.label}: ${reader.description}${windows}${params}`\n })\n .join('\\n')\n}\n\n/** The datasets a datasets question may name, as the read prompt lists them. */\nexport function aiInsightDatasetCatalog(datasets: readonly AiDatasetCatalogEntry[]): string {\n if (!datasets.length) return 'There are no datasets you may read here.'\n return datasets\n .map(\n (dataset) =>\n `- ${dataset.name} (id ${dataset.id}): ${dataset.fields.map((field) => `${field.name} [${field.type}]`).join(', ') || 'no fields'}`,\n )\n .join('\\n')\n}\n\nexport interface AiInsightReadResult {\n tables: AiInsightTable[]\n /** Why a request that named a real reader was not read, in its owner's words, for the log. */\n refusals: string[]\n}\n\n/**\n * The tables for the reads a job asked for: each request held to the readers\n * this job may read, its window to that reader's, its parameters to those it\n * declares; each table held to the contract by `normalizePluginFigureTable`.\n * A reader asked for twice with the same window and parameters is read once.\n */\nexport async function readAiInsightTables(\n readers: readonly ResolvedPluginFigureReader[],\n requests: readonly AiInsightReadRequest[],\n base: { orgId: string; hostId: string | null; uid: string | null; now: Date },\n maxTables: number = AI_INSIGHT_MAX_READS,\n): Promise<AiInsightReadResult> {\n const tables: AiInsightTable[] = []\n const refusals: string[] = []\n const seen = new Set<string>()\n for (const request of requests) {\n if (tables.length === maxTables) break\n const resolved = readers.find(({ reader }) => reader.id === request.reader)\n if (!resolved) continue\n const { reader } = resolved\n const days = aiInsightReaderDays(reader.windows, request.days)\n const params = pluginFigureParams(reader, request.params)\n if (params.ok === false) {\n refusals.push(`${reader.id}: missing ${params.missing}`)\n continue\n }\n const key = JSON.stringify([reader.id, days, params.params])\n if (seen.has(key)) continue\n seen.add(key)\n try {\n const read = await reader.read({ ...base, days, params: params.params })\n if (read.ok === false) {\n refusals.push(`${reader.id}: ${read.error}`)\n continue\n }\n const table = normalizePluginFigureTable(read.table)\n if (!table) {\n refusals.push(`${reader.id}: not a table`)\n continue\n }\n tables.push({ ...table, ref: `t${tables.length + 1}`, reader: reader.id, days, scope: reader.scope })\n } catch (error) {\n // One reader's fault is not the answer's: the rest are still read.\n console.error('ai insight reader failed', { reader: reader.id, orgId: base.orgId, error })\n refusals.push(`${reader.id}: failed`)\n }\n }\n return { tables, refusals }\n}\n\n/** One table as the answer prompt shows it: a heading, the columns, numbered rows, and its notes. */\nexport function aiInsightTableText(table: AiInsightTable): string {\n const period = table.period ? ` · ${table.period.from} to ${table.period.to} (${table.period.days} days)` : ''\n const lines = [\n `${table.ref} · ${table.title} (${table.source.label})${period}`,\n `Columns: ${table.columns.map((column) => column.label).join(' | ')}`,\n ...table.rows.map(\n (row, index) =>\n `${index}: ${table.columns.map((column) => aiInsightCellText(table, column.key, row[column.key])).join(' | ')}`,\n ),\n ]\n if (!table.rows.length) lines.push('(no rows)')\n if (table.omitted) lines.push(`${table.omitted} more rows were left out of this table.`)\n for (const note of table.notes) lines.push(`Note: ${note}`)\n return lines.join('\\n')\n}\n"],"names":["normalizePluginFigureTable","pluginFigureParams","AI_INSIGHT_MAX_READS","AI_INSIGHT_SURFACE_READERS","aiInsightCellText","eligibleFigureReaders","aiInsightReaders","context","prefixes","surface","orgId","hostId","org","host","accept","reader","includes","id","split","released","aiInsightReaderDays","windows","asked","length","sort","a","b","Math","abs","aiInsightReaderCatalog","readers","map","join","params","param","name","required","description","label","aiInsightDatasetCatalog","datasets","dataset","fields","field","type","readAiInsightTables","requests","base","maxTables","tables","refusals","seen","Set","request","resolved","find","days","ok","push","missing","key","JSON","stringify","has","add","read","error","table","ref","scope","console","aiInsightTableText","period","from","to","lines","title","source","columns","column","rows","row","index","omitted","note","notes"],"mappings":"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"}
|
|
@@ -26,7 +26,7 @@ export declare const AI_INSIGHT_NO_READERS_COPY = "There are no figures to answe
|
|
|
26
26
|
export declare const AI_INSIGHT_NO_TABLES_COPY = "The figures could not be read. Try again in a moment.";
|
|
27
27
|
export declare const AI_INSIGHT_UNREADABLE_COPY = "The AI did not answer with insights. Try asking in different words.";
|
|
28
28
|
/** The digest's question: what a week's insights cover, in the order a person reads them. */
|
|
29
|
-
export declare const AI_INSIGHT_DIGEST_QUESTION = "What changed on this site
|
|
29
|
+
export declare const AI_INSIGHT_DIGEST_QUESTION = "What changed on this site last week, Monday to Sunday? Write the week\u2019s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out \u2014 only those the tables support.";
|
|
30
30
|
/** The rules both calls are sent: static, byte-identical for every workspace. */
|
|
31
31
|
export declare const AI_INSIGHT_RULES: string;
|
|
32
32
|
/** The system blocks both calls send: the rules and the platform's acceptable-use rules. */
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
-
import { AI_INSIGHT_ASK_SURFACES, AI_INSIGHT_DIGEST_DAYS, AI_INSIGHT_DIGEST_READERS, AI_INSIGHT_QUESTION_MAX_CHARS, AI_INSIGHTS_COLLECTION, aiInsightDays, aiInsightOutput, aiInsightSurface, aiInsightSurfaceNeedsSite } from "../model/ai-insight.js";
|
|
17
|
+
import { AI_INSIGHT_ASK_SURFACES, AI_INSIGHT_DIGEST_DAYS, AI_INSIGHT_DIGEST_READERS, AI_INSIGHT_QUESTION_MAX_CHARS, AI_INSIGHTS_COLLECTION, aiInsightDays, aiInsightDigestPeriod, aiInsightOutput, aiInsightSurface, aiInsightSurfaceNeedsSite } from "../model/ai-insight.js";
|
|
18
18
|
import { aiHostPublishContext, aiSiteStatusTable } from "../model/ai-host-publish-context.js";
|
|
19
19
|
import { aiInstructionsWithSiteContext } from "../model/ai-site-context.js";
|
|
20
20
|
import { readAiSiteContext } from "../runtime/site-context.js";
|
|
@@ -35,7 +35,7 @@ export const AI_INSIGHT_NO_SITE_COPY = 'Open the site whose numbers you are aski
|
|
|
35
35
|
export const AI_INSIGHT_NO_READERS_COPY = 'There are no figures to answer from here yet: the features this page reports on are not on for this workspace.';
|
|
36
36
|
export const AI_INSIGHT_NO_TABLES_COPY = 'The figures could not be read. Try again in a moment.';
|
|
37
37
|
export const AI_INSIGHT_UNREADABLE_COPY = 'The AI did not answer with insights. Try asking in different words.';
|
|
38
|
-
/** The digest's question: what a week's insights cover, in the order a person reads them. */ export const AI_INSIGHT_DIGEST_QUESTION = 'What changed on this site
|
|
38
|
+
/** The digest's question: what a week's insights cover, in the order a person reads them. */ export const AI_INSIGHT_DIGEST_QUESTION = 'What changed on this site last week, Monday to Sunday? Write the week’s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out — only those the tables support.';
|
|
39
39
|
/** The rules both calls are sent: static, byte-identical for every workspace. */ export const AI_INSIGHT_RULES = [
|
|
40
40
|
'You answer a workspace owner’s questions about their own figures: page views, orders, bookings, forms, campaigns, A/B tests and datasets. You never see a record, and you never write a query: code reads figures through fixed readers and gives you their tables.',
|
|
41
41
|
'',
|
|
@@ -215,11 +215,13 @@ export const runAiJobInsightStep = async (context)=>{
|
|
|
215
215
|
signal
|
|
216
216
|
} : {}));
|
|
217
217
|
// 1. The reads.
|
|
218
|
+
// A digest reads last Monday to Sunday, whatever minute the beat runs it.
|
|
219
|
+
const readNow = digest ? aiInsightDigestPeriod(job.inputs, now).now : now;
|
|
218
220
|
const base = {
|
|
219
221
|
orgId: job.orgId,
|
|
220
222
|
hostId,
|
|
221
223
|
uid: digest ? null : job.createdBy,
|
|
222
|
-
now
|
|
224
|
+
now: readNow
|
|
223
225
|
};
|
|
224
226
|
let requests;
|
|
225
227
|
if (digest) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-insight-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 type { ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_ASK_SURFACES,\n AI_INSIGHT_DIGEST_DAYS,\n AI_INSIGHT_DIGEST_READERS,\n AI_INSIGHT_QUESTION_MAX_CHARS,\n AI_INSIGHTS_COLLECTION,\n aiInsightDays,\n aiInsightOutput,\n aiInsightSurface,\n aiInsightSurfaceNeedsSite,\n type AiInsightRecord,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport { aiHostPublishContext, aiSiteStatusTable } from '../model/ai-host-publish-context'\nimport { aiInstructionsWithSiteContext } from '../model/ai-site-context'\nimport { readAiSiteContext } from '../runtime/site-context'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { aiDevEffortFor } from '../runtime/ai-dev-env'\nimport {\n AI_ACCEPTABLE_USE_BLOCK,\n runAiRequest,\n type AiMessage,\n type AiResult,\n type AiSystemBlock,\n type AiUsage,\n} from '../runtime/ai-runtime'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport { checkAiInsightAnswer, type AiInsightCheck } from '../runtime/ai-insight-check'\nimport {\n AI_INSIGHT_ANSWER_TOOL_NAME,\n AI_INSIGHT_READ_TOOL_NAME,\n aiInsightAnswerTool,\n aiInsightReadTool,\n parseAiInsightAnswer,\n parseAiInsightReads,\n type AiInsightReadRequest,\n} from '../tools/ai-insight-tool'\nimport { aiDatasetCatalog } from '../insights/ai-dataset-catalog'\nimport {\n aiInsightDatasetCatalog,\n aiInsightReaderCatalog,\n aiInsightReaderDays,\n aiInsightReaders,\n aiInsightTableText,\n readAiInsightTables,\n} from '../insights/ai-insight-readers'\nimport { assistExchangeExpiry } from '../usage/assist-usage'\nimport { registerAiJobAdmission, type AiJobAdmissionContext, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { AI_INSIGHT_READ_MAX_TOKENS, AI_JOB_INSIGHT_STEP_BUDGET, AI_JOB_INSIGHT_STEP_MINIMUM_MS } from './ai-job-insight-budget'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `insight` step (AGL-2915): a question about a site's figures, answered\n * from tables code read, with every insight traced to the rows it cites.\n *\n * ## Two exchanges, and the model never writes a query\n *\n * 1. **The reads.** The model is shown the question and the readers this job\n * may read — each an id, what it answers, its windows and parameters — and\n * answers `read_figures` with a choice among them. It names nothing else: no\n * collection, no field path, no filter. A choice that names no reader this\n * job may read is dropped, and a job left with none reads the surface's\n * defaults. A weekly digest makes no read call: code reads the digest's\n * readers.\n * 2. **The answer.** Code reads the tables through the owners' readers, and the\n * model is shown them and answers `submit_insights`. The trace\n * (`runtime/ai-insight-check.ts`) keeps each insight whose every number is\n * in a row it cites and leaves out the rest. An answer that is not a call,\n * or whose every insight was left out, is asked for once more with the\n * reasons; what the second answer keeps is the answer.\n *\n * Both calls share one allowance of output, twice the routing ceiling, which\n * is what the step's least time is planned against.\n *\n * ## What reaches the model\n *\n * The question, the names and descriptions of the readers, the tables, the\n * site's publish state (published or not, its address, its pages' paths), and\n * for a datasets question the names of the datasets the member may see with\n * their fields' names and types. A table holds counts, sums and rates labeled\n * by a page path, a referring site, a form's, a service's, a product's or a\n * campaign's name, or the values of a dataset field with at most sixty\n * different values that at least three records share. No\n * record, visitor, contact, order, booking, submission or recipient is read\n * into a prompt, and every text cell is masked of email addresses and phone\n * numbers on its way into a table.\n *\n * ## What it writes\n *\n * The answer, at `orgs/{orgId}/aiInsights/{jobId}` under the job's own\n * expiry, and an output that names it and says how many insights it holds.\n * Nothing else: no draft, no setting, no message.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const AI_INSIGHT_NO_SURFACE_COPY = 'This question does not say which page it was asked from.'\nexport const AI_INSIGHT_NO_SITE_COPY = 'Open the site whose numbers you are asking about.'\nexport const AI_INSIGHT_NO_READERS_COPY =\n 'There are no figures to answer from here yet: the features this page reports on are not on for this workspace.'\nexport const AI_INSIGHT_NO_TABLES_COPY = 'The figures could not be read. Try again in a moment.'\nexport const AI_INSIGHT_UNREADABLE_COPY = 'The AI did not answer with insights. Try asking in different words.'\n\n/** The digest's question: what a week's insights cover, in the order a person reads them. */\nexport const AI_INSIGHT_DIGEST_QUESTION =\n 'What changed on this site this week? Write the week’s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out — only those the tables support.'\n\n/** The rules both calls are sent: static, byte-identical for every workspace. */\nexport const AI_INSIGHT_RULES = [\n 'You answer a workspace owner’s questions about their own figures: page views, orders, bookings, forms, campaigns, A/B tests and datasets. You never see a record, and you never write a query: code reads figures through fixed readers and gives you their tables.',\n '',\n `When you are given a question and a list of readers, call ${AI_INSIGHT_READ_TOOL_NAME} once with the readers whose tables answer it, most useful first, and the window each should cover. Choose only readers from the list, with their parameters' values taken from what the list and the question give you.`,\n '',\n `When you are given tables, call ${AI_INSIGHT_ANSWER_TOOL_NAME} once. Each table has a handle (t1, t2, …) and numbered rows. Rules for the insights:`,\n '- Say what matters most to the question first. Leave out what the tables do not support.',\n '- Every insight cites the table and the rows its figures come from.',\n '- Every number you write appears in a row you cite, as the table shows it or rounded. Never add, subtract, average or otherwise compute a figure yourself. The only other numbers you may write are the length of a table’s window, a date in it, and how many rows you cite.',\n '- Say a figure rose or fell only when a change you quote shows it.',\n '- Name nobody, and write no email address or phone number.',\n '- Plain sentences: no markdown, no headings, no lists.',\n '- When the tables cannot answer part of the question, say so in gap in one sentence with no numbers; otherwise gap is null.',\n '- Whether the site is published, its address and which pages are published come only from the Site status table. Page views and visitors never show whether a site or a page is live: never say or suggest a site is not live, not launched or not published because of its traffic, and never call the pages a views table lists the published ones.',\n '- Write in the language of the question.',\n].join('\\n')\n\n/** The system blocks both calls send: the rules and the platform's acceptable-use rules. */\nexport const AI_JOB_INSIGHT_SYSTEM: AiSystemBlock[] = [\n { text: `${AI_INSIGHT_RULES}\\n\\n${AI_ACCEPTABLE_USE_BLOCK}`, cacheBreakpoint: true },\n]\n\nconst ZERO_USAGE: AiUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What the step's calls have spent so far, in the outcome's shape. */\nclass InsightSpend {\n usage: AiUsage = { ...ZERO_USAGE }\n estCostUsd = 0\n stopReason: string | null = null\n\n constructor(readonly model: string) {}\n\n add(result: AiResult): void {\n this.usage = {\n inputTokens: this.usage.inputTokens + result.usage.inputTokens,\n outputTokens: this.usage.outputTokens + result.usage.outputTokens,\n cacheReadTokens: this.usage.cacheReadTokens + result.usage.cacheReadTokens,\n cacheWriteTokens: this.usage.cacheWriteTokens + result.usage.cacheWriteTokens,\n }\n this.estCostUsd = Math.round((this.estCostUsd + result.estCostUsd) * 1_000_000) / 1_000_000\n this.stopReason = result.stopReason\n }\n\n outcome(): Pick<AiJobStepOutcome, 'usage' | 'estCostUsd' | 'model' | 'stopReason' | 'effort'> {\n return { usage: this.usage, estCostUsd: this.estCostUsd, model: this.model, stopReason: this.stopReason, effort: null }\n }\n}\n\n/** The read call's user turn: the question, the window asked for, the readers and, for datasets, the datasets. */\nexport function aiInsightReadPrompt(input: {\n question: string\n days: number\n readers: readonly ResolvedPluginFigureReader[]\n datasets: string | null\n}): string {\n return [\n `Question: ${input.question}`,\n `The window picked on the page: ${input.days} days. Use it unless the question names another window a reader lists.`,\n 'Readers you may read here:',\n aiInsightReaderCatalog(input.readers),\n ...(input.datasets === null ? [] : ['', 'Datasets you may read, with their fields:', input.datasets]),\n ].join('\\n')\n}\n\n/** The answer call's user turn: the question and every table. */\nexport function aiInsightAnswerPrompt(question: string, tables: readonly AiInsightTable[]): string {\n return [`Question: ${question}`, '', 'Tables:', '', tables.map(aiInsightTableText).join('\\n\\n')].join('\\n')\n}\n\n/** What a re-ask tells the model: why nothing it wrote could be kept. */\nexport function aiInsightReaskMessage(check: AiInsightCheck | null): string {\n const reasons = check?.findings.length\n ? `None of those insights could be kept: ${[...new Set(check.findings)].slice(0, 8).join('; ')}.`\n : `That answer did not come through ${AI_INSIGHT_ANSWER_TOOL_NAME}.`\n return `${reasons} Answer again with ${AI_INSIGHT_ANSWER_TOOL_NAME}: cite the rows each insight's figures are in, and write every number as a row you cite shows it.`\n}\n\n/** The reads a surface falls back to when the model named none it may read: the digest's own readers. */\nfunction defaultReads(readers: readonly ResolvedPluginFigureReader[], days: number): AiInsightReadRequest[] {\n const ids = readers.map(({ reader }) => reader.id)\n const preferred = AI_INSIGHT_DIGEST_READERS.filter((id) => ids.includes(id))\n const chosen = preferred.length ? preferred : ids\n return chosen.map((id) => ({\n reader: id,\n days: aiInsightReaderDays(readers.find(({ reader }) => reader.id === id)?.reader.windows ?? [], days),\n params: {},\n }))\n}\n\nasync function readHost(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n): Promise<{ host: Record<string, unknown> | null; ok: boolean }> {\n if (!hostId) return { host: null, ok: true }\n const snapshot = await firestore.collection('hosts').doc(hostId).get()\n const host = snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n return { host, ok: Boolean(host) && host?.['orgId'] === orgId }\n}\n\nexport const runAiJobInsightStep: AiJobStepRunner = async (context: AiJobStepContext) => {\n const { job, firestore, signal, now } = context\n const model = context.modelFor?.('job.insight') ?? aiModelForStep('job.insight')\n const spend = new InsightSpend(model)\n const stop = (failure: string): AiJobStepOutcome => ({ outputs: [], ...spend.outcome(), failure })\n\n const surface = aiInsightSurface(job.inputs)\n if (!surface) return stop(AI_INSIGHT_NO_SURFACE_COPY)\n const hostId = job.hostId ?? null\n if (aiInsightSurfaceNeedsSite(surface) && !hostId) return stop(AI_INSIGHT_NO_SITE_COPY)\n // The door checks the caller belongs to the org; that the site does is\n // checked here, before anything about it is read.\n const site = await readHost(firestore, job.orgId, hostId)\n if (!site.ok) return stop(AI_INSIGHT_NO_SITE_COPY)\n const readers = await aiInsightReaders({\n orgId: job.orgId,\n hostId,\n org: context.org ?? null,\n host: site.host,\n surface,\n })\n if (!readers.length) return stop(AI_INSIGHT_NO_READERS_COPY)\n\n const digest = surface === 'digest'\n const days = digest ? AI_INSIGHT_DIGEST_DAYS : aiInsightDays(job.inputs?.['days'])\n const question = digest ? AI_INSIGHT_DIGEST_QUESTION : job.brief.slice(0, AI_INSIGHT_QUESTION_MAX_CHARS)\n const maxTokens = AI_JOB_INSIGHT_STEP_BUDGET.maxTokens(model)\n const allowance = AI_GENERATION_MAX_ATTEMPTS * maxTokens\n const left = () => allowance - spend.usage.outputTokens\n // What the business is and what the owner's edits showed (AGL-3661), so an\n // answer reads the numbers for THIS business. No contact details: an\n // insight names nobody. No publish state either: that is the Site status\n // table's to say, and the rules send the model there.\n const siteContext = await readAiSiteContext(firestore, { orgId: job.orgId, hostId, host: site.host ?? null })\n const system = [\n ...aiInstructionsWithSiteContext(\n AI_JOB_INSIGHT_SYSTEM,\n siteContext ? { ...siteContext, publish: null, contact: false } : null,\n ),\n ]\n // A development run's effort A/B arm (AGL-3781), else the row's.\n const effort = aiDevEffortFor('job.insight') ?? AI_ROUTING_TABLE['job.insight'].effort\n const ask = (messages: AiMessage[], tool: 'read' | 'answer', ceiling: number) =>\n runAiRequest({\n model,\n system,\n messages,\n tools: [tool === 'read' ? aiInsightReadTool() : aiInsightAnswerTool()],\n maxTokens: Math.max(1, Math.min(ceiling, left())),\n ...(AI_ROUTING_TABLE['job.insight'].thinking ? { thinking: AI_ROUTING_TABLE['job.insight'].thinking } : {}),\n ...(effort ? { effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n\n // 1. The reads.\n const base = { orgId: job.orgId, hostId, uid: digest ? null : job.createdBy, now }\n let requests: AiInsightReadRequest[]\n if (digest) {\n requests = defaultReads(readers, days)\n } else {\n const datasets =\n surface === 'datasets'\n ? aiInsightDatasetCatalog(await aiDatasetCatalog(firestore, { orgId: job.orgId, hostId, uid: job.createdBy }))\n : null\n const result = await ask(\n [{ role: 'user', content: aiInsightReadPrompt({ question, days, readers, datasets }) }],\n 'read',\n AI_INSIGHT_READ_MAX_TOKENS,\n )\n spend.add(result)\n if (result.kind === 'refusal') return { outputs: [], ...spend.outcome(), refused: true }\n requests = parseAiInsightReads(result.toolUse.find((use) => use.name === AI_INSIGHT_READ_TOOL_NAME)?.input)\n }\n const maxTables = digest ? AI_INSIGHT_DIGEST_READERS.length : undefined\n let read = await readAiInsightTables(readers, requests, base, maxTables)\n if (!read.tables.length && !digest) read = await readAiInsightTables(readers, defaultReads(readers, days), base)\n if (read.refusals.length) console.warn('ai insight reads refused', { jobId: job.$id, refusals: read.refusals })\n if (!read.tables.length) return stop(AI_INSIGHT_NO_TABLES_COPY)\n // A site's question is told whether the site is live, so traffic is never\n // read as launch status. Appended, so the readers' refs stay as they were.\n const tables =\n !digest && site.host\n ? [...read.tables, aiSiteStatusTable(aiHostPublishContext(site.host), `t${read.tables.length + 1}`)]\n : read.tables\n\n // 2. The answer, and at most one re-ask.\n let messages: AiMessage[] = [{ role: 'user', content: aiInsightAnswerPrompt(question, tables) }]\n let checked: AiInsightCheck | null = null\n for (let attempt = 0; attempt < AI_GENERATION_MAX_ATTEMPTS && left() > 0; attempt += 1) {\n const result = await ask(messages, 'answer', maxTokens)\n spend.add(result)\n if (result.kind === 'refusal') return { outputs: [], ...spend.outcome(), refused: true }\n const answer = parseAiInsightAnswer(result.toolUse.find((use) => use.name === AI_INSIGHT_ANSWER_TOOL_NAME)?.input)\n const check = answer ? checkAiInsightAnswer(answer, tables) : null\n if (check) checked = check\n if (check && (check.kept.length || !answer?.insights.length)) break\n messages = [\n ...messages,\n { role: 'assistant', content: `Answered with ${AI_INSIGHT_ANSWER_TOOL_NAME}.` },\n { role: 'user', content: aiInsightReaskMessage(check) },\n ]\n }\n if (!checked) return stop(AI_INSIGHT_UNREADABLE_COPY)\n\n const record: AiInsightRecord = {\n jobId: job.$id,\n orgId: job.orgId,\n hostId,\n surface,\n createdBy: job.createdBy,\n question: digest ? str(job.brief) || 'Weekly insights' : question,\n days,\n insights: checked.kept,\n gap: checked.gap,\n tables,\n left: checked.left.length,\n week: digest ? str(job.inputs?.['week']) || null : null,\n createdAtMs: now.getTime(),\n }\n if (checked.left.length) {\n console.info('ai insight left out untraced insights', { jobId: job.$id, findings: checked.findings })\n }\n // Keyed by the job, so a step run again replaces its own answer.\n await firestore\n .collection('orgs')\n .doc(job.orgId)\n .collection(AI_INSIGHTS_COLLECTION)\n .doc(job.$id)\n .set({ ...(JSON.parse(JSON.stringify(record)) as AiInsightRecord), expiresAt: job.expiresAt ?? assistExchangeExpiry(now) })\n return {\n outputs: [aiInsightOutput(job, record, str(site.host?.['subdomain']) || null)],\n ...spend.outcome(),\n }\n}\n\n/**\n * Whether a question may be asked here: a surface a person asks from, a site\n * of the job's own org where the surface needs one, and at least one reader\n * the workspace may read. A digest is made by the weekly sweep and never at a\n * door.\n */\nexport async function aiInsightAdmissionRefusal(\n context: AiJobAdmissionContext,\n): Promise<AiJobAdmissionRefusal | null> {\n const surface: AiInsightSurface | null = aiInsightSurface(context.inputs)\n if (!surface || !AI_INSIGHT_ASK_SURFACES.includes(surface)) {\n return { status: 400, error: 'Ask about your numbers from a site’s Analytics, Data or CRM Reports page.' }\n }\n if (aiInsightSurfaceNeedsSite(surface) && !context.hostId) {\n return { status: 400, error: AI_INSIGHT_NO_SITE_COPY }\n }\n const site = await readHost(context.firestore, context.orgId, context.hostId)\n if (!site.ok) return { status: 404, error: 'Unknown site' }\n const readers = await aiInsightReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host: site.host,\n surface,\n })\n return readers.length ? null : { status: 403, error: AI_INSIGHT_NO_READERS_COPY }\n}\n\n/**\n * The insight kind's registrations, called by the console surface\n * (`registerAiJobKinds` in `server.ts`), never made by importing this module\n * (AGL-3025).\n */\nexport function registerAiInsightJob(): void {\n registerAiJobStep('insight', runAiJobInsightStep, { minimumMs: AI_JOB_INSIGHT_STEP_MINIMUM_MS })\n registerAiJobAdmission('insight', aiInsightAdmissionRefusal)\n}\n"],"names":["AI_INSIGHT_ASK_SURFACES","AI_INSIGHT_DIGEST_DAYS","AI_INSIGHT_DIGEST_READERS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHTS_COLLECTION","aiInsightDays","aiInsightOutput","aiInsightSurface","aiInsightSurfaceNeedsSite","aiHostPublishContext","aiSiteStatusTable","aiInstructionsWithSiteContext","readAiSiteContext","AI_ROUTING_TABLE","aiModelForStep","aiDevEffortFor","AI_ACCEPTABLE_USE_BLOCK","runAiRequest","AI_GENERATION_MAX_ATTEMPTS","checkAiInsightAnswer","AI_INSIGHT_ANSWER_TOOL_NAME","AI_INSIGHT_READ_TOOL_NAME","aiInsightAnswerTool","aiInsightReadTool","parseAiInsightAnswer","parseAiInsightReads","aiDatasetCatalog","aiInsightDatasetCatalog","aiInsightReaderCatalog","aiInsightReaderDays","aiInsightReaders","aiInsightTableText","readAiInsightTables","assistExchangeExpiry","registerAiJobAdmission","AI_INSIGHT_READ_MAX_TOKENS","AI_JOB_INSIGHT_STEP_BUDGET","AI_JOB_INSIGHT_STEP_MINIMUM_MS","registerAiJobStep","AI_INSIGHT_NO_SURFACE_COPY","AI_INSIGHT_NO_SITE_COPY","AI_INSIGHT_NO_READERS_COPY","AI_INSIGHT_NO_TABLES_COPY","AI_INSIGHT_UNREADABLE_COPY","AI_INSIGHT_DIGEST_QUESTION","AI_INSIGHT_RULES","join","AI_JOB_INSIGHT_SYSTEM","text","cacheBreakpoint","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","str","value","trim","InsightSpend","add","result","usage","estCostUsd","Math","round","stopReason","outcome","model","effort","aiInsightReadPrompt","input","question","days","readers","datasets","aiInsightAnswerPrompt","tables","map","aiInsightReaskMessage","check","reasons","findings","length","Set","slice","defaultReads","ids","reader","id","preferred","filter","includes","chosen","find","windows","params","readHost","firestore","orgId","hostId","snapshot","host","ok","collection","doc","get","exists","data","Boolean","runAiJobInsightStep","context","job","site","signal","now","modelFor","spend","stop","failure","outputs","surface","inputs","org","digest","brief","maxTokens","allowance","left","siteContext","system","publish","contact","ask","messages","tool","ceiling","tools","max","min","thinking","stream","base","uid","createdBy","requests","role","content","kind","refused","toolUse","use","name","maxTables","undefined","read","refusals","console","warn","jobId","$id","checked","attempt","answer","kept","insights","record","gap","week","createdAtMs","getTime","info","set","JSON","parse","stringify","expiresAt","aiInsightAdmissionRefusal","status","error","registerAiInsightJob","minimumMs"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-insight-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 type { ResolvedPluginFigureReader } from '@aglyn/aglyn/plugin-manager/plugin-figures'\nimport {\n AI_INSIGHT_ASK_SURFACES,\n AI_INSIGHT_DIGEST_DAYS,\n AI_INSIGHT_DIGEST_READERS,\n AI_INSIGHT_QUESTION_MAX_CHARS,\n AI_INSIGHTS_COLLECTION,\n aiInsightDays,\n aiInsightDigestPeriod,\n aiInsightOutput,\n aiInsightSurface,\n aiInsightSurfaceNeedsSite,\n type AiInsightRecord,\n type AiInsightSurface,\n type AiInsightTable,\n} from '../model/ai-insight'\nimport { aiHostPublishContext, aiSiteStatusTable } from '../model/ai-host-publish-context'\nimport { aiInstructionsWithSiteContext } from '../model/ai-site-context'\nimport { readAiSiteContext } from '../runtime/site-context'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { aiDevEffortFor } from '../runtime/ai-dev-env'\nimport {\n AI_ACCEPTABLE_USE_BLOCK,\n runAiRequest,\n type AiMessage,\n type AiResult,\n type AiSystemBlock,\n type AiUsage,\n} from '../runtime/ai-runtime'\nimport { AI_GENERATION_MAX_ATTEMPTS } from '../runtime/ai-generation-bounds'\nimport { checkAiInsightAnswer, type AiInsightCheck } from '../runtime/ai-insight-check'\nimport {\n AI_INSIGHT_ANSWER_TOOL_NAME,\n AI_INSIGHT_READ_TOOL_NAME,\n aiInsightAnswerTool,\n aiInsightReadTool,\n parseAiInsightAnswer,\n parseAiInsightReads,\n type AiInsightReadRequest,\n} from '../tools/ai-insight-tool'\nimport { aiDatasetCatalog } from '../insights/ai-dataset-catalog'\nimport {\n aiInsightDatasetCatalog,\n aiInsightReaderCatalog,\n aiInsightReaderDays,\n aiInsightReaders,\n aiInsightTableText,\n readAiInsightTables,\n} from '../insights/ai-insight-readers'\nimport { assistExchangeExpiry } from '../usage/assist-usage'\nimport { registerAiJobAdmission, type AiJobAdmissionContext, type AiJobAdmissionRefusal } from './ai-job-admission'\nimport { AI_INSIGHT_READ_MAX_TOKENS, AI_JOB_INSIGHT_STEP_BUDGET, AI_JOB_INSIGHT_STEP_MINIMUM_MS } from './ai-job-insight-budget'\nimport type { AiJobStepContext, AiJobStepOutcome, AiJobStepRunner } from './ai-job-text-step'\nimport { registerAiJobStep } from './ai-jobs'\n\n/**\n * The `insight` step (AGL-2915): a question about a site's figures, answered\n * from tables code read, with every insight traced to the rows it cites.\n *\n * ## Two exchanges, and the model never writes a query\n *\n * 1. **The reads.** The model is shown the question and the readers this job\n * may read — each an id, what it answers, its windows and parameters — and\n * answers `read_figures` with a choice among them. It names nothing else: no\n * collection, no field path, no filter. A choice that names no reader this\n * job may read is dropped, and a job left with none reads the surface's\n * defaults. A weekly digest makes no read call: code reads the digest's\n * readers.\n * 2. **The answer.** Code reads the tables through the owners' readers, and the\n * model is shown them and answers `submit_insights`. The trace\n * (`runtime/ai-insight-check.ts`) keeps each insight whose every number is\n * in a row it cites and leaves out the rest. An answer that is not a call,\n * or whose every insight was left out, is asked for once more with the\n * reasons; what the second answer keeps is the answer.\n *\n * Both calls share one allowance of output, twice the routing ceiling, which\n * is what the step's least time is planned against.\n *\n * ## What reaches the model\n *\n * The question, the names and descriptions of the readers, the tables, the\n * site's publish state (published or not, its address, its pages' paths), and\n * for a datasets question the names of the datasets the member may see with\n * their fields' names and types. A table holds counts, sums and rates labeled\n * by a page path, a referring site, a form's, a service's, a product's or a\n * campaign's name, or the values of a dataset field with at most sixty\n * different values that at least three records share. No\n * record, visitor, contact, order, booking, submission or recipient is read\n * into a prompt, and every text cell is masked of email addresses and phone\n * numbers on its way into a table.\n *\n * ## What it writes\n *\n * The answer, at `orgs/{orgId}/aiInsights/{jobId}` under the job's own\n * expiry, and an output that names it and says how many insights it holds.\n * Nothing else: no draft, no setting, no message.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const AI_INSIGHT_NO_SURFACE_COPY = 'This question does not say which page it was asked from.'\nexport const AI_INSIGHT_NO_SITE_COPY = 'Open the site whose numbers you are asking about.'\nexport const AI_INSIGHT_NO_READERS_COPY =\n 'There are no figures to answer from here yet: the features this page reports on are not on for this workspace.'\nexport const AI_INSIGHT_NO_TABLES_COPY = 'The figures could not be read. Try again in a moment.'\nexport const AI_INSIGHT_UNREADABLE_COPY = 'The AI did not answer with insights. Try asking in different words.'\n\n/** The digest's question: what a week's insights cover, in the order a person reads them. */\nexport const AI_INSIGHT_DIGEST_QUESTION =\n 'What changed on this site last week, Monday to Sunday? Write the week’s insights: the change in traffic, the most viewed page, the form that turned the most views into submissions, a campaign that did better than the rest, and any day that stood out — only those the tables support.'\n\n/** The rules both calls are sent: static, byte-identical for every workspace. */\nexport const AI_INSIGHT_RULES = [\n 'You answer a workspace owner’s questions about their own figures: page views, orders, bookings, forms, campaigns, A/B tests and datasets. You never see a record, and you never write a query: code reads figures through fixed readers and gives you their tables.',\n '',\n `When you are given a question and a list of readers, call ${AI_INSIGHT_READ_TOOL_NAME} once with the readers whose tables answer it, most useful first, and the window each should cover. Choose only readers from the list, with their parameters' values taken from what the list and the question give you.`,\n '',\n `When you are given tables, call ${AI_INSIGHT_ANSWER_TOOL_NAME} once. Each table has a handle (t1, t2, …) and numbered rows. Rules for the insights:`,\n '- Say what matters most to the question first. Leave out what the tables do not support.',\n '- Every insight cites the table and the rows its figures come from.',\n '- Every number you write appears in a row you cite, as the table shows it or rounded. Never add, subtract, average or otherwise compute a figure yourself. The only other numbers you may write are the length of a table’s window, a date in it, and how many rows you cite.',\n '- Say a figure rose or fell only when a change you quote shows it.',\n '- Name nobody, and write no email address or phone number.',\n '- Plain sentences: no markdown, no headings, no lists.',\n '- When the tables cannot answer part of the question, say so in gap in one sentence with no numbers; otherwise gap is null.',\n '- Whether the site is published, its address and which pages are published come only from the Site status table. Page views and visitors never show whether a site or a page is live: never say or suggest a site is not live, not launched or not published because of its traffic, and never call the pages a views table lists the published ones.',\n '- Write in the language of the question.',\n].join('\\n')\n\n/** The system blocks both calls send: the rules and the platform's acceptable-use rules. */\nexport const AI_JOB_INSIGHT_SYSTEM: AiSystemBlock[] = [\n { text: `${AI_INSIGHT_RULES}\\n\\n${AI_ACCEPTABLE_USE_BLOCK}`, cacheBreakpoint: true },\n]\n\nconst ZERO_USAGE: AiUsage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 }\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')\n\n/** What the step's calls have spent so far, in the outcome's shape. */\nclass InsightSpend {\n usage: AiUsage = { ...ZERO_USAGE }\n estCostUsd = 0\n stopReason: string | null = null\n\n constructor(readonly model: string) {}\n\n add(result: AiResult): void {\n this.usage = {\n inputTokens: this.usage.inputTokens + result.usage.inputTokens,\n outputTokens: this.usage.outputTokens + result.usage.outputTokens,\n cacheReadTokens: this.usage.cacheReadTokens + result.usage.cacheReadTokens,\n cacheWriteTokens: this.usage.cacheWriteTokens + result.usage.cacheWriteTokens,\n }\n this.estCostUsd = Math.round((this.estCostUsd + result.estCostUsd) * 1_000_000) / 1_000_000\n this.stopReason = result.stopReason\n }\n\n outcome(): Pick<AiJobStepOutcome, 'usage' | 'estCostUsd' | 'model' | 'stopReason' | 'effort'> {\n return { usage: this.usage, estCostUsd: this.estCostUsd, model: this.model, stopReason: this.stopReason, effort: null }\n }\n}\n\n/** The read call's user turn: the question, the window asked for, the readers and, for datasets, the datasets. */\nexport function aiInsightReadPrompt(input: {\n question: string\n days: number\n readers: readonly ResolvedPluginFigureReader[]\n datasets: string | null\n}): string {\n return [\n `Question: ${input.question}`,\n `The window picked on the page: ${input.days} days. Use it unless the question names another window a reader lists.`,\n 'Readers you may read here:',\n aiInsightReaderCatalog(input.readers),\n ...(input.datasets === null ? [] : ['', 'Datasets you may read, with their fields:', input.datasets]),\n ].join('\\n')\n}\n\n/** The answer call's user turn: the question and every table. */\nexport function aiInsightAnswerPrompt(question: string, tables: readonly AiInsightTable[]): string {\n return [`Question: ${question}`, '', 'Tables:', '', tables.map(aiInsightTableText).join('\\n\\n')].join('\\n')\n}\n\n/** What a re-ask tells the model: why nothing it wrote could be kept. */\nexport function aiInsightReaskMessage(check: AiInsightCheck | null): string {\n const reasons = check?.findings.length\n ? `None of those insights could be kept: ${[...new Set(check.findings)].slice(0, 8).join('; ')}.`\n : `That answer did not come through ${AI_INSIGHT_ANSWER_TOOL_NAME}.`\n return `${reasons} Answer again with ${AI_INSIGHT_ANSWER_TOOL_NAME}: cite the rows each insight's figures are in, and write every number as a row you cite shows it.`\n}\n\n/** The reads a surface falls back to when the model named none it may read: the digest's own readers. */\nfunction defaultReads(readers: readonly ResolvedPluginFigureReader[], days: number): AiInsightReadRequest[] {\n const ids = readers.map(({ reader }) => reader.id)\n const preferred = AI_INSIGHT_DIGEST_READERS.filter((id) => ids.includes(id))\n const chosen = preferred.length ? preferred : ids\n return chosen.map((id) => ({\n reader: id,\n days: aiInsightReaderDays(readers.find(({ reader }) => reader.id === id)?.reader.windows ?? [], days),\n params: {},\n }))\n}\n\nasync function readHost(\n firestore: Firestore,\n orgId: string,\n hostId: string | null,\n): Promise<{ host: Record<string, unknown> | null; ok: boolean }> {\n if (!hostId) return { host: null, ok: true }\n const snapshot = await firestore.collection('hosts').doc(hostId).get()\n const host = snapshot.exists ? ((snapshot.data() ?? {}) as Record<string, unknown>) : null\n return { host, ok: Boolean(host) && host?.['orgId'] === orgId }\n}\n\nexport const runAiJobInsightStep: AiJobStepRunner = async (context: AiJobStepContext) => {\n const { job, firestore, signal, now } = context\n const model = context.modelFor?.('job.insight') ?? aiModelForStep('job.insight')\n const spend = new InsightSpend(model)\n const stop = (failure: string): AiJobStepOutcome => ({ outputs: [], ...spend.outcome(), failure })\n\n const surface = aiInsightSurface(job.inputs)\n if (!surface) return stop(AI_INSIGHT_NO_SURFACE_COPY)\n const hostId = job.hostId ?? null\n if (aiInsightSurfaceNeedsSite(surface) && !hostId) return stop(AI_INSIGHT_NO_SITE_COPY)\n // The door checks the caller belongs to the org; that the site does is\n // checked here, before anything about it is read.\n const site = await readHost(firestore, job.orgId, hostId)\n if (!site.ok) return stop(AI_INSIGHT_NO_SITE_COPY)\n const readers = await aiInsightReaders({\n orgId: job.orgId,\n hostId,\n org: context.org ?? null,\n host: site.host,\n surface,\n })\n if (!readers.length) return stop(AI_INSIGHT_NO_READERS_COPY)\n\n const digest = surface === 'digest'\n const days = digest ? AI_INSIGHT_DIGEST_DAYS : aiInsightDays(job.inputs?.['days'])\n const question = digest ? AI_INSIGHT_DIGEST_QUESTION : job.brief.slice(0, AI_INSIGHT_QUESTION_MAX_CHARS)\n const maxTokens = AI_JOB_INSIGHT_STEP_BUDGET.maxTokens(model)\n const allowance = AI_GENERATION_MAX_ATTEMPTS * maxTokens\n const left = () => allowance - spend.usage.outputTokens\n // What the business is and what the owner's edits showed (AGL-3661), so an\n // answer reads the numbers for THIS business. No contact details: an\n // insight names nobody. No publish state either: that is the Site status\n // table's to say, and the rules send the model there.\n const siteContext = await readAiSiteContext(firestore, { orgId: job.orgId, hostId, host: site.host ?? null })\n const system = [\n ...aiInstructionsWithSiteContext(\n AI_JOB_INSIGHT_SYSTEM,\n siteContext ? { ...siteContext, publish: null, contact: false } : null,\n ),\n ]\n // A development run's effort A/B arm (AGL-3781), else the row's.\n const effort = aiDevEffortFor('job.insight') ?? AI_ROUTING_TABLE['job.insight'].effort\n const ask = (messages: AiMessage[], tool: 'read' | 'answer', ceiling: number) =>\n runAiRequest({\n model,\n system,\n messages,\n tools: [tool === 'read' ? aiInsightReadTool() : aiInsightAnswerTool()],\n maxTokens: Math.max(1, Math.min(ceiling, left())),\n ...(AI_ROUTING_TABLE['job.insight'].thinking ? { thinking: AI_ROUTING_TABLE['job.insight'].thinking } : {}),\n ...(effort ? { effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n\n // 1. The reads.\n // A digest reads last Monday to Sunday, whatever minute the beat runs it.\n const readNow = digest ? aiInsightDigestPeriod(job.inputs, now).now : now\n const base = { orgId: job.orgId, hostId, uid: digest ? null : job.createdBy, now: readNow }\n let requests: AiInsightReadRequest[]\n if (digest) {\n requests = defaultReads(readers, days)\n } else {\n const datasets =\n surface === 'datasets'\n ? aiInsightDatasetCatalog(await aiDatasetCatalog(firestore, { orgId: job.orgId, hostId, uid: job.createdBy }))\n : null\n const result = await ask(\n [{ role: 'user', content: aiInsightReadPrompt({ question, days, readers, datasets }) }],\n 'read',\n AI_INSIGHT_READ_MAX_TOKENS,\n )\n spend.add(result)\n if (result.kind === 'refusal') return { outputs: [], ...spend.outcome(), refused: true }\n requests = parseAiInsightReads(result.toolUse.find((use) => use.name === AI_INSIGHT_READ_TOOL_NAME)?.input)\n }\n const maxTables = digest ? AI_INSIGHT_DIGEST_READERS.length : undefined\n let read = await readAiInsightTables(readers, requests, base, maxTables)\n if (!read.tables.length && !digest) read = await readAiInsightTables(readers, defaultReads(readers, days), base)\n if (read.refusals.length) console.warn('ai insight reads refused', { jobId: job.$id, refusals: read.refusals })\n if (!read.tables.length) return stop(AI_INSIGHT_NO_TABLES_COPY)\n // A site's question is told whether the site is live, so traffic is never\n // read as launch status. Appended, so the readers' refs stay as they were.\n const tables =\n !digest && site.host\n ? [...read.tables, aiSiteStatusTable(aiHostPublishContext(site.host), `t${read.tables.length + 1}`)]\n : read.tables\n\n // 2. The answer, and at most one re-ask.\n let messages: AiMessage[] = [{ role: 'user', content: aiInsightAnswerPrompt(question, tables) }]\n let checked: AiInsightCheck | null = null\n for (let attempt = 0; attempt < AI_GENERATION_MAX_ATTEMPTS && left() > 0; attempt += 1) {\n const result = await ask(messages, 'answer', maxTokens)\n spend.add(result)\n if (result.kind === 'refusal') return { outputs: [], ...spend.outcome(), refused: true }\n const answer = parseAiInsightAnswer(result.toolUse.find((use) => use.name === AI_INSIGHT_ANSWER_TOOL_NAME)?.input)\n const check = answer ? checkAiInsightAnswer(answer, tables) : null\n if (check) checked = check\n if (check && (check.kept.length || !answer?.insights.length)) break\n messages = [\n ...messages,\n { role: 'assistant', content: `Answered with ${AI_INSIGHT_ANSWER_TOOL_NAME}.` },\n { role: 'user', content: aiInsightReaskMessage(check) },\n ]\n }\n if (!checked) return stop(AI_INSIGHT_UNREADABLE_COPY)\n\n const record: AiInsightRecord = {\n jobId: job.$id,\n orgId: job.orgId,\n hostId,\n surface,\n createdBy: job.createdBy,\n question: digest ? str(job.brief) || 'Weekly insights' : question,\n days,\n insights: checked.kept,\n gap: checked.gap,\n tables,\n left: checked.left.length,\n week: digest ? str(job.inputs?.['week']) || null : null,\n createdAtMs: now.getTime(),\n }\n if (checked.left.length) {\n console.info('ai insight left out untraced insights', { jobId: job.$id, findings: checked.findings })\n }\n // Keyed by the job, so a step run again replaces its own answer.\n await firestore\n .collection('orgs')\n .doc(job.orgId)\n .collection(AI_INSIGHTS_COLLECTION)\n .doc(job.$id)\n .set({ ...(JSON.parse(JSON.stringify(record)) as AiInsightRecord), expiresAt: job.expiresAt ?? assistExchangeExpiry(now) })\n return {\n outputs: [aiInsightOutput(job, record, str(site.host?.['subdomain']) || null)],\n ...spend.outcome(),\n }\n}\n\n/**\n * Whether a question may be asked here: a surface a person asks from, a site\n * of the job's own org where the surface needs one, and at least one reader\n * the workspace may read. A digest is made by the weekly sweep and never at a\n * door.\n */\nexport async function aiInsightAdmissionRefusal(\n context: AiJobAdmissionContext,\n): Promise<AiJobAdmissionRefusal | null> {\n const surface: AiInsightSurface | null = aiInsightSurface(context.inputs)\n if (!surface || !AI_INSIGHT_ASK_SURFACES.includes(surface)) {\n return { status: 400, error: 'Ask about your numbers from a site’s Analytics, Data or CRM Reports page.' }\n }\n if (aiInsightSurfaceNeedsSite(surface) && !context.hostId) {\n return { status: 400, error: AI_INSIGHT_NO_SITE_COPY }\n }\n const site = await readHost(context.firestore, context.orgId, context.hostId)\n if (!site.ok) return { status: 404, error: 'Unknown site' }\n const readers = await aiInsightReaders({\n orgId: context.orgId,\n hostId: context.hostId,\n org: context.org,\n host: site.host,\n surface,\n })\n return readers.length ? null : { status: 403, error: AI_INSIGHT_NO_READERS_COPY }\n}\n\n/**\n * The insight kind's registrations, called by the console surface\n * (`registerAiJobKinds` in `server.ts`), never made by importing this module\n * (AGL-3025).\n */\nexport function registerAiInsightJob(): void {\n registerAiJobStep('insight', runAiJobInsightStep, { minimumMs: AI_JOB_INSIGHT_STEP_MINIMUM_MS })\n registerAiJobAdmission('insight', aiInsightAdmissionRefusal)\n}\n"],"names":["AI_INSIGHT_ASK_SURFACES","AI_INSIGHT_DIGEST_DAYS","AI_INSIGHT_DIGEST_READERS","AI_INSIGHT_QUESTION_MAX_CHARS","AI_INSIGHTS_COLLECTION","aiInsightDays","aiInsightDigestPeriod","aiInsightOutput","aiInsightSurface","aiInsightSurfaceNeedsSite","aiHostPublishContext","aiSiteStatusTable","aiInstructionsWithSiteContext","readAiSiteContext","AI_ROUTING_TABLE","aiModelForStep","aiDevEffortFor","AI_ACCEPTABLE_USE_BLOCK","runAiRequest","AI_GENERATION_MAX_ATTEMPTS","checkAiInsightAnswer","AI_INSIGHT_ANSWER_TOOL_NAME","AI_INSIGHT_READ_TOOL_NAME","aiInsightAnswerTool","aiInsightReadTool","parseAiInsightAnswer","parseAiInsightReads","aiDatasetCatalog","aiInsightDatasetCatalog","aiInsightReaderCatalog","aiInsightReaderDays","aiInsightReaders","aiInsightTableText","readAiInsightTables","assistExchangeExpiry","registerAiJobAdmission","AI_INSIGHT_READ_MAX_TOKENS","AI_JOB_INSIGHT_STEP_BUDGET","AI_JOB_INSIGHT_STEP_MINIMUM_MS","registerAiJobStep","AI_INSIGHT_NO_SURFACE_COPY","AI_INSIGHT_NO_SITE_COPY","AI_INSIGHT_NO_READERS_COPY","AI_INSIGHT_NO_TABLES_COPY","AI_INSIGHT_UNREADABLE_COPY","AI_INSIGHT_DIGEST_QUESTION","AI_INSIGHT_RULES","join","AI_JOB_INSIGHT_SYSTEM","text","cacheBreakpoint","ZERO_USAGE","inputTokens","outputTokens","cacheReadTokens","cacheWriteTokens","str","value","trim","InsightSpend","add","result","usage","estCostUsd","Math","round","stopReason","outcome","model","effort","aiInsightReadPrompt","input","question","days","readers","datasets","aiInsightAnswerPrompt","tables","map","aiInsightReaskMessage","check","reasons","findings","length","Set","slice","defaultReads","ids","reader","id","preferred","filter","includes","chosen","find","windows","params","readHost","firestore","orgId","hostId","snapshot","host","ok","collection","doc","get","exists","data","Boolean","runAiJobInsightStep","context","job","site","signal","now","modelFor","spend","stop","failure","outputs","surface","inputs","org","digest","brief","maxTokens","allowance","left","siteContext","system","publish","contact","ask","messages","tool","ceiling","tools","max","min","thinking","stream","readNow","base","uid","createdBy","requests","role","content","kind","refused","toolUse","use","name","maxTables","undefined","read","refusals","console","warn","jobId","$id","checked","attempt","answer","kept","insights","record","gap","week","createdAtMs","getTime","info","set","JSON","parse","stringify","expiresAt","aiInsightAdmissionRefusal","status","error","registerAiInsightJob","minimumMs"],"mappings":"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"}
|
|
@@ -221,6 +221,8 @@ export interface AiLayoutPageCheckInput {
|
|
|
221
221
|
kind?: string;
|
|
222
222
|
seed: number;
|
|
223
223
|
};
|
|
224
|
+
/** Whether copy may promise what is coming (a coming-soon site, AGL-3887); absent, it may not. */
|
|
225
|
+
promisesAllowed?: boolean;
|
|
224
226
|
}
|
|
225
227
|
/**
|
|
226
228
|
* The follow-up turn for the sections a page's answer left out (AGL-3660):
|