@anthusai/papyrus 0.0.0 → 1.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2069 -0
- package/amplify/data/resource.ts +2185 -0
- package/amplify/data/schema.js +1882 -0
- package/amplify/data/schema.ts +1990 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/content-actions/handler.py +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +130 -0
- package/amplify/functions/shared/python-bundle.ts +162 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +36 -0
- package/amplify/site-backend.js +615 -0
- package/amplify/site-backend.ts +739 -0
- package/amplify/storage/resource.js +21 -0
- package/amplify/storage/resource.ts +22 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10345 -0
- package/app/layout.tsx +142 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/papyrus-theme.css +11 -0
- package/app/robots.ts +11 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +20 -0
- package/bin/papyrus-infra.mjs +95 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +156 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +116 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +321 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +18005 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +234 -0
- package/infra/amplify-app-shell.ts +284 -0
- package/infra/build-specs.js +85 -0
- package/infra/build-specs.ts +131 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +15 -0
- package/infra/site-config.js +186 -0
- package/infra/site-config.ts +235 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +90 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +43 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/empty-theme.css +1 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +1140 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +44 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +22 -0
- package/lib/site-brand.ts +194 -0
- package/lib/site-env.ts +52 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +103 -0
- package/package.json +318 -1
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +169 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +470 -0
- package/routes.manifest.json +208 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +60 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|
3
|
+
|
|
4
|
+
type DataClient = {
|
|
5
|
+
models: {
|
|
6
|
+
ModelAttachment: {
|
|
7
|
+
get(input: { id: string }): Promise<{ data?: any | null; errors?: unknown }>;
|
|
8
|
+
create(input: Record<string, unknown>): Promise<{ data?: any | null; errors?: unknown }>;
|
|
9
|
+
update(input: Record<string, unknown>): Promise<{ data?: any | null; errors?: unknown }>;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const s3 = new S3Client({});
|
|
15
|
+
|
|
16
|
+
export type ModelPayloadOwner = {
|
|
17
|
+
ownerKind: string;
|
|
18
|
+
ownerId: string;
|
|
19
|
+
ownerLineageId?: string | null;
|
|
20
|
+
ownerVersionNumber?: number | null;
|
|
21
|
+
ownerVersionKey?: string | null;
|
|
22
|
+
importRunId?: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export async function putJsonModelPayload(
|
|
26
|
+
client: DataClient,
|
|
27
|
+
owner: ModelPayloadOwner,
|
|
28
|
+
role: string,
|
|
29
|
+
sortKey: string,
|
|
30
|
+
payload: unknown,
|
|
31
|
+
options: { filename?: string; now?: string; status?: string } = {},
|
|
32
|
+
) {
|
|
33
|
+
const body = `${JSON.stringify(payload ?? {}, null, 2)}\n`;
|
|
34
|
+
return putModelPayload(client, owner, role, sortKey, body, {
|
|
35
|
+
filename: options.filename ?? `${safeId(sortKey || role)}.json`,
|
|
36
|
+
mediaType: "application/json",
|
|
37
|
+
now: options.now,
|
|
38
|
+
status: options.status,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function putTextModelPayload(
|
|
43
|
+
client: DataClient,
|
|
44
|
+
owner: ModelPayloadOwner,
|
|
45
|
+
role: string,
|
|
46
|
+
sortKey: string,
|
|
47
|
+
body: string,
|
|
48
|
+
options: { filename?: string; mediaType?: string; now?: string; status?: string } = {},
|
|
49
|
+
) {
|
|
50
|
+
return putModelPayload(client, owner, role, sortKey, body, {
|
|
51
|
+
filename: options.filename ?? `${safeId(sortKey || role)}.txt`,
|
|
52
|
+
mediaType: options.mediaType ?? "text/plain",
|
|
53
|
+
now: options.now,
|
|
54
|
+
status: options.status,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function putModelPayload(
|
|
59
|
+
client: DataClient,
|
|
60
|
+
owner: ModelPayloadOwner,
|
|
61
|
+
role: string,
|
|
62
|
+
sortKey: string,
|
|
63
|
+
body: string,
|
|
64
|
+
options: { filename: string; mediaType: string; now?: string; status?: string },
|
|
65
|
+
) {
|
|
66
|
+
const now = options.now ?? new Date().toISOString();
|
|
67
|
+
const storagePath = modelPayloadStoragePath(owner.ownerKind, owner.ownerId, role, options.filename);
|
|
68
|
+
const buffer = Buffer.from(body, "utf8");
|
|
69
|
+
const sha256 = createHash("sha256").update(buffer).digest("hex");
|
|
70
|
+
await s3.send(new PutObjectCommand({
|
|
71
|
+
Bucket: payloadBucketName(),
|
|
72
|
+
Key: storagePath,
|
|
73
|
+
Body: buffer,
|
|
74
|
+
ContentType: options.mediaType,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const id = modelAttachmentId(owner.ownerKind, owner.ownerId, role, sortKey);
|
|
78
|
+
const input = {
|
|
79
|
+
id,
|
|
80
|
+
ownerKind: owner.ownerKind,
|
|
81
|
+
ownerId: owner.ownerId,
|
|
82
|
+
ownerLineageId: owner.ownerLineageId ?? null,
|
|
83
|
+
ownerVersionNumber: owner.ownerVersionNumber ?? null,
|
|
84
|
+
ownerVersionKey: owner.ownerVersionKey ?? null,
|
|
85
|
+
role,
|
|
86
|
+
sortKey,
|
|
87
|
+
storagePath,
|
|
88
|
+
filename: options.filename,
|
|
89
|
+
mediaType: options.mediaType,
|
|
90
|
+
byteSize: buffer.length,
|
|
91
|
+
sha256,
|
|
92
|
+
etag: null,
|
|
93
|
+
importRunId: owner.importRunId ?? null,
|
|
94
|
+
createdAt: now,
|
|
95
|
+
updatedAt: now,
|
|
96
|
+
status: options.status ?? "active",
|
|
97
|
+
};
|
|
98
|
+
const current = await client.models.ModelAttachment.get({ id });
|
|
99
|
+
assertNoDataErrors(current.errors, "get ModelAttachment");
|
|
100
|
+
if (current.data) {
|
|
101
|
+
await requireDataResult(client.models.ModelAttachment.update({ ...input, createdAt: current.data.createdAt ?? now }), "update ModelAttachment");
|
|
102
|
+
} else {
|
|
103
|
+
await requireDataResult(client.models.ModelAttachment.create(input), "create ModelAttachment");
|
|
104
|
+
}
|
|
105
|
+
return input;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function readJsonModelPayload(
|
|
109
|
+
client: DataClient,
|
|
110
|
+
ownerKind: string,
|
|
111
|
+
ownerId: string,
|
|
112
|
+
role: string,
|
|
113
|
+
sortKey = role,
|
|
114
|
+
) {
|
|
115
|
+
const result = await client.models.ModelAttachment.get({ id: modelAttachmentId(ownerKind, ownerId, role, sortKey) });
|
|
116
|
+
assertNoDataErrors(result.errors, "get ModelAttachment");
|
|
117
|
+
const attachment = result.data;
|
|
118
|
+
if (!attachment?.storagePath) return null;
|
|
119
|
+
const text = await readTextFromStorage(attachment.storagePath);
|
|
120
|
+
if (!text) return null;
|
|
121
|
+
const parsed = JSON.parse(text);
|
|
122
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function readTextFromStorage(storagePath: string): Promise<string> {
|
|
126
|
+
try {
|
|
127
|
+
const result = await s3.send(new GetObjectCommand({
|
|
128
|
+
Bucket: payloadBucketName(),
|
|
129
|
+
Key: storagePath,
|
|
130
|
+
}));
|
|
131
|
+
return result.Body?.transformToString("utf8") ?? "";
|
|
132
|
+
} catch (error) {
|
|
133
|
+
if (isMissingStorageKeyError(error)) return "";
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isMissingStorageKeyError(error: unknown): boolean {
|
|
139
|
+
if (!error || typeof error !== "object") return false;
|
|
140
|
+
const record = error as Record<string, unknown>;
|
|
141
|
+
if (record.name === "NoSuchKey" || record.Code === "NoSuchKey") return true;
|
|
142
|
+
const message = typeof record.message === "string" ? record.message : "";
|
|
143
|
+
return message.toLowerCase().includes("the specified key does not exist");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function payloadBucketName(): string {
|
|
147
|
+
const bucket = process.env.papyrusMedia_BUCKET_NAME
|
|
148
|
+
?? process.env.PAPYRUS_MEDIA_BUCKET_NAME
|
|
149
|
+
?? process.env.STORAGE_BUCKET_NAME
|
|
150
|
+
?? process.env.AMPLIFY_STORAGE_BUCKET_NAME;
|
|
151
|
+
if (!bucket) throw new Error("Missing Papyrus storage bucket environment variable for model payload attachments.");
|
|
152
|
+
return bucket;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function modelPayloadStoragePath(ownerKind: string, ownerId: string, role: string, filename: string): string {
|
|
156
|
+
const rootPrefix = ownerKind === "publishedItem" ? "media/payloads" : "newsroom/payloads";
|
|
157
|
+
return `${rootPrefix}/${safeId(ownerKind)}/${safeId(ownerId)}/${safeId(role)}/${filename}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function modelAttachmentId(ownerKind: string, ownerId: string, role: string, sortKey: string): string {
|
|
161
|
+
return `model-attachment-${safeId(ownerKind)}-${safeId(ownerId)}-${safeId(role)}-${safeId(sortKey)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function safeId(value: unknown): string {
|
|
165
|
+
const raw = String(value ?? "payload").trim();
|
|
166
|
+
const normalized = raw
|
|
167
|
+
.trim()
|
|
168
|
+
.toLowerCase()
|
|
169
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
170
|
+
.replace(/^-+|-+$/g, "") || "payload";
|
|
171
|
+
if (normalized.length <= 80) return normalized;
|
|
172
|
+
const digest = createHash("sha256").update(raw).digest("hex").slice(0, 12);
|
|
173
|
+
return `${normalized.slice(0, 67).replace(/-+$/g, "")}-${digest}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function requireDataResult<T>(promise: Promise<{ data?: T | null; errors?: unknown }>, context: string): Promise<T | null | undefined> {
|
|
177
|
+
const result = await promise;
|
|
178
|
+
assertNoDataErrors(result.errors, context);
|
|
179
|
+
return result.data;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function assertNoDataErrors(errors: unknown, context: string): void {
|
|
183
|
+
if (Array.isArray(errors) && errors.length) {
|
|
184
|
+
throw new Error(`${context}: ${errors.map((error) => typeof error === "string" ? error : JSON.stringify(error)).join("; ")}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { AssetHashType, DockerImage } from "aws-cdk-lib";
|
|
2
|
+
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
/**
|
|
8
|
+
* Python Lambda bundling that works both in a Papyrus checkout and when
|
|
9
|
+
* `@anthusai/papyrus` is installed from npm into a publication repo.
|
|
10
|
+
*
|
|
11
|
+
* - Checkout mode (`src/` exists next to `amplify/`, default in the Papyrus
|
|
12
|
+
* repo): copy `src/<module>` into the asset, as before.
|
|
13
|
+
* - Package mode (no `src/`, or PAPYRUS_BUNDLE_FROM_PIP=1): the asset is
|
|
14
|
+
* `pip install papyrus-newsroom==<this package's version>` plus the
|
|
15
|
+
* function's requirements, targeted at the Lambda platform (linux aarch64,
|
|
16
|
+
* CPython 3.12, binary wheels only); the handler is copied from this package.
|
|
17
|
+
*
|
|
18
|
+
* PAPYRUS_PIP_EXTRA_ARGS lets a site point pip at a private index or a local
|
|
19
|
+
* wheel directory (e.g. `--find-links ./vendor`).
|
|
20
|
+
*/
|
|
21
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
export const packageRoot = path.resolve(here, "../../..");
|
|
23
|
+
export function isPackageMode() {
|
|
24
|
+
if (process.env.PAPYRUS_BUNDLE_FROM_PIP === "1")
|
|
25
|
+
return true;
|
|
26
|
+
return !fs.existsSync(path.join(packageRoot, "src", "papyrus_content"));
|
|
27
|
+
}
|
|
28
|
+
/** Lockstep version: the npm package's own version is the Python pin. */
|
|
29
|
+
export function papyrusVersion() {
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
31
|
+
return pkg.version;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* semver -> PEP 440. `1.2.0-next.3` is not a valid Python version; Semantic
|
|
35
|
+
* Release prereleases (`-next.N`) map to dev releases (`1.2.0.dev3`), which sort
|
|
36
|
+
* before the final release exactly like semver prereleases do.
|
|
37
|
+
*/
|
|
38
|
+
export function toPep440(version) {
|
|
39
|
+
const m = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z-]+\.(\d+))?$/.exec(version);
|
|
40
|
+
if (!m)
|
|
41
|
+
throw new Error(`Cannot map ${version} to a PEP 440 version`);
|
|
42
|
+
return m[2] === undefined ? m[1] : `${m[1]}.dev${m[2]}`;
|
|
43
|
+
}
|
|
44
|
+
function pipSpec() {
|
|
45
|
+
return (process.env.PAPYRUS_PIP_SPEC ?? "").trim() || `papyrus-newsroom==${toPep440(papyrusVersion())}`;
|
|
46
|
+
}
|
|
47
|
+
function bundleFromPip(outputDir, options) {
|
|
48
|
+
const extra = (process.env.PAPYRUS_PIP_EXTRA_ARGS ?? "").trim().split(/\s+/).filter(Boolean);
|
|
49
|
+
const python = (process.env.PAPYRUS_PIP_PYTHON ?? "python3").trim();
|
|
50
|
+
execFileSync(python, [
|
|
51
|
+
"-m", "pip", "install",
|
|
52
|
+
"--target", outputDir,
|
|
53
|
+
"--no-cache-dir",
|
|
54
|
+
"--platform", "manylinux2014_aarch64",
|
|
55
|
+
"--implementation", "cp",
|
|
56
|
+
"--python-version", "3.12",
|
|
57
|
+
"--only-binary=:all:",
|
|
58
|
+
// Older pip (e.g. a Python 3.9 build image) checks Requires-Python against the host interpreter, not --python-version.
|
|
59
|
+
"--ignore-requires-python",
|
|
60
|
+
...extra,
|
|
61
|
+
...(options.noDeps ? [] : [pipSpec()]),
|
|
62
|
+
...(options.requirements ?? []),
|
|
63
|
+
], { stdio: "inherit" });
|
|
64
|
+
if (options.noDeps) {
|
|
65
|
+
execFileSync(python, [
|
|
66
|
+
"-m", "pip", "install",
|
|
67
|
+
"--target", outputDir,
|
|
68
|
+
"--no-cache-dir",
|
|
69
|
+
"--no-deps",
|
|
70
|
+
"--platform", "manylinux2014_aarch64",
|
|
71
|
+
"--implementation", "cp",
|
|
72
|
+
"--python-version", "3.12",
|
|
73
|
+
"--only-binary=:all:",
|
|
74
|
+
"--ignore-requires-python",
|
|
75
|
+
...extra,
|
|
76
|
+
pipSpec(),
|
|
77
|
+
], { stdio: "inherit" });
|
|
78
|
+
}
|
|
79
|
+
fs.copyFileSync(path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"), path.join(outputDir, "handler.py"));
|
|
80
|
+
if (options.corpora) {
|
|
81
|
+
const corporaDir = path.join(outputDir, "corpora");
|
|
82
|
+
fs.mkdirSync(corporaDir, { recursive: true });
|
|
83
|
+
const sourceDir = path.join(process.cwd(), "corpora");
|
|
84
|
+
if (fs.existsSync(sourceDir)) {
|
|
85
|
+
for (const entry of fs.readdirSync(sourceDir)) {
|
|
86
|
+
if (entry.endsWith(".yml"))
|
|
87
|
+
fs.copyFileSync(path.join(sourceDir, entry), path.join(corporaDir, entry));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function bundleFromCheckout(outputDir, options) {
|
|
93
|
+
fs.copyFileSync(path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"), path.join(outputDir, "handler.py"));
|
|
94
|
+
for (const module of options.modules) {
|
|
95
|
+
fs.mkdirSync(path.join(outputDir, module), { recursive: true });
|
|
96
|
+
fs.cpSync(path.join(packageRoot, "src", module), path.join(outputDir, module), { recursive: true });
|
|
97
|
+
}
|
|
98
|
+
options.checkoutExtras?.(outputDir);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* CDK Code for a Python Lambda. In package mode the asset hash is derived
|
|
102
|
+
* from the pinned version and pip spec instead of hashing the package root.
|
|
103
|
+
*/
|
|
104
|
+
export function pythonLambdaCode(options, dockerCommand) {
|
|
105
|
+
const packageMode = isPackageMode();
|
|
106
|
+
return Code.fromAsset(packageRoot, {
|
|
107
|
+
...(packageMode
|
|
108
|
+
? {
|
|
109
|
+
assetHashType: AssetHashType.CUSTOM,
|
|
110
|
+
assetHash: `${options.functionDir}:${pipSpec()}:${(options.requirements ?? []).join(",")}`
|
|
111
|
+
.replace(/[^a-zA-Z0-9._:=,-]/g, "_"),
|
|
112
|
+
}
|
|
113
|
+
: {}),
|
|
114
|
+
bundling: {
|
|
115
|
+
image: Runtime.PYTHON_3_12.bundlingImage ?? DockerImage.fromRegistry("public.ecr.aws/sam/build-python3.12"),
|
|
116
|
+
local: {
|
|
117
|
+
tryBundle(outputDir) {
|
|
118
|
+
if (packageMode)
|
|
119
|
+
bundleFromPip(outputDir, options);
|
|
120
|
+
else
|
|
121
|
+
bundleFromCheckout(outputDir, options);
|
|
122
|
+
return true;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
command: packageMode
|
|
126
|
+
? ["bash", "-c", "echo 'Package-mode Python bundling runs locally (pip on the build host).' >&2; exit 1"]
|
|
127
|
+
: dockerCommand,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { AssetHashType, DockerImage } from "aws-cdk-lib";
|
|
2
|
+
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Python Lambda bundling that works both in a Papyrus checkout and when
|
|
10
|
+
* `@anthusai/papyrus` is installed from npm into a publication repo.
|
|
11
|
+
*
|
|
12
|
+
* - Checkout mode (`src/` exists next to `amplify/`, default in the Papyrus
|
|
13
|
+
* repo): copy `src/<module>` into the asset, as before.
|
|
14
|
+
* - Package mode (no `src/`, or PAPYRUS_BUNDLE_FROM_PIP=1): the asset is
|
|
15
|
+
* `pip install papyrus-newsroom==<this package's version>` plus the
|
|
16
|
+
* function's requirements, targeted at the Lambda platform (linux aarch64,
|
|
17
|
+
* CPython 3.12, binary wheels only); the handler is copied from this package.
|
|
18
|
+
*
|
|
19
|
+
* PAPYRUS_PIP_EXTRA_ARGS lets a site point pip at a private index or a local
|
|
20
|
+
* wheel directory (e.g. `--find-links ./vendor`).
|
|
21
|
+
*/
|
|
22
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
export const packageRoot = path.resolve(here, "../../..");
|
|
24
|
+
|
|
25
|
+
export function isPackageMode(): boolean {
|
|
26
|
+
if (process.env.PAPYRUS_BUNDLE_FROM_PIP === "1") return true;
|
|
27
|
+
return !fs.existsSync(path.join(packageRoot, "src", "papyrus_content"));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Lockstep version: the npm package's own version is the Python pin. */
|
|
31
|
+
export function papyrusVersion(): string {
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")) as { version: string };
|
|
33
|
+
return pkg.version;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* semver -> PEP 440. `1.2.0-next.3` is not a valid Python version; Semantic
|
|
38
|
+
* Release prereleases (`-next.N`) map to dev releases (`1.2.0.dev3`), which sort
|
|
39
|
+
* before the final release exactly like semver prereleases do.
|
|
40
|
+
*/
|
|
41
|
+
export function toPep440(version: string): string {
|
|
42
|
+
const m = /^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z-]+\.(\d+))?$/.exec(version);
|
|
43
|
+
if (!m) throw new Error(`Cannot map ${version} to a PEP 440 version`);
|
|
44
|
+
return m[2] === undefined ? m[1] : `${m[1]}.dev${m[2]}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type PythonBundleOptions = {
|
|
48
|
+
/** Directory under `amplify/functions/` holding `handler.py`. */
|
|
49
|
+
functionDir: string;
|
|
50
|
+
/** `src/` modules to copy in checkout mode. */
|
|
51
|
+
modules: string[];
|
|
52
|
+
/** Run extra checkout-mode steps (e.g. copy corpora, pip install -r). */
|
|
53
|
+
checkoutExtras?: (outputDir: string) => void;
|
|
54
|
+
/** Extra pip requirements in package mode, beyond `papyrus-newsroom`. */
|
|
55
|
+
requirements?: string[];
|
|
56
|
+
/** Package mode: install `papyrus-newsroom` with `--no-deps`, then only `requirements`. */
|
|
57
|
+
noDeps?: boolean;
|
|
58
|
+
/** Copy `<cwd>/corpora/*.yml` into the asset (publication-owned steering config). */
|
|
59
|
+
corpora?: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function pipSpec(): string {
|
|
63
|
+
return (process.env.PAPYRUS_PIP_SPEC ?? "").trim() || `papyrus-newsroom==${toPep440(papyrusVersion())}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function bundleFromPip(outputDir: string, options: PythonBundleOptions): void {
|
|
67
|
+
const extra = (process.env.PAPYRUS_PIP_EXTRA_ARGS ?? "").trim().split(/\s+/).filter(Boolean);
|
|
68
|
+
const python = (process.env.PAPYRUS_PIP_PYTHON ?? "python3").trim();
|
|
69
|
+
execFileSync(
|
|
70
|
+
python,
|
|
71
|
+
[
|
|
72
|
+
"-m", "pip", "install",
|
|
73
|
+
"--target", outputDir,
|
|
74
|
+
"--no-cache-dir",
|
|
75
|
+
"--platform", "manylinux2014_aarch64",
|
|
76
|
+
"--implementation", "cp",
|
|
77
|
+
"--python-version", "3.12",
|
|
78
|
+
"--only-binary=:all:",
|
|
79
|
+
// Older pip (e.g. a Python 3.9 build image) checks Requires-Python against the host interpreter, not --python-version.
|
|
80
|
+
"--ignore-requires-python",
|
|
81
|
+
...extra,
|
|
82
|
+
...(options.noDeps ? [] : [pipSpec()]),
|
|
83
|
+
...(options.requirements ?? []),
|
|
84
|
+
],
|
|
85
|
+
{ stdio: "inherit" },
|
|
86
|
+
);
|
|
87
|
+
if (options.noDeps) {
|
|
88
|
+
execFileSync(
|
|
89
|
+
python,
|
|
90
|
+
[
|
|
91
|
+
"-m", "pip", "install",
|
|
92
|
+
"--target", outputDir,
|
|
93
|
+
"--no-cache-dir",
|
|
94
|
+
"--no-deps",
|
|
95
|
+
"--platform", "manylinux2014_aarch64",
|
|
96
|
+
"--implementation", "cp",
|
|
97
|
+
"--python-version", "3.12",
|
|
98
|
+
"--only-binary=:all:",
|
|
99
|
+
"--ignore-requires-python",
|
|
100
|
+
...extra,
|
|
101
|
+
pipSpec(),
|
|
102
|
+
],
|
|
103
|
+
{ stdio: "inherit" },
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
fs.copyFileSync(
|
|
107
|
+
path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"),
|
|
108
|
+
path.join(outputDir, "handler.py"),
|
|
109
|
+
);
|
|
110
|
+
if (options.corpora) {
|
|
111
|
+
const corporaDir = path.join(outputDir, "corpora");
|
|
112
|
+
fs.mkdirSync(corporaDir, { recursive: true });
|
|
113
|
+
const sourceDir = path.join(process.cwd(), "corpora");
|
|
114
|
+
if (fs.existsSync(sourceDir)) {
|
|
115
|
+
for (const entry of fs.readdirSync(sourceDir)) {
|
|
116
|
+
if (entry.endsWith(".yml")) fs.copyFileSync(path.join(sourceDir, entry), path.join(corporaDir, entry));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function bundleFromCheckout(outputDir: string, options: PythonBundleOptions): void {
|
|
123
|
+
fs.copyFileSync(
|
|
124
|
+
path.join(packageRoot, "amplify/functions", options.functionDir, "handler.py"),
|
|
125
|
+
path.join(outputDir, "handler.py"),
|
|
126
|
+
);
|
|
127
|
+
for (const module of options.modules) {
|
|
128
|
+
fs.mkdirSync(path.join(outputDir, module), { recursive: true });
|
|
129
|
+
fs.cpSync(path.join(packageRoot, "src", module), path.join(outputDir, module), { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
options.checkoutExtras?.(outputDir);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* CDK Code for a Python Lambda. In package mode the asset hash is derived
|
|
136
|
+
* from the pinned version and pip spec instead of hashing the package root.
|
|
137
|
+
*/
|
|
138
|
+
export function pythonLambdaCode(options: PythonBundleOptions, dockerCommand: string[]): Code {
|
|
139
|
+
const packageMode = isPackageMode();
|
|
140
|
+
return Code.fromAsset(packageRoot, {
|
|
141
|
+
...(packageMode
|
|
142
|
+
? {
|
|
143
|
+
assetHashType: AssetHashType.CUSTOM,
|
|
144
|
+
assetHash: `${options.functionDir}:${pipSpec()}:${(options.requirements ?? []).join(",")}`
|
|
145
|
+
.replace(/[^a-zA-Z0-9._:=,-]/g, "_"),
|
|
146
|
+
}
|
|
147
|
+
: {}),
|
|
148
|
+
bundling: {
|
|
149
|
+
image: Runtime.PYTHON_3_12.bundlingImage ?? DockerImage.fromRegistry("public.ecr.aws/sam/build-python3.12"),
|
|
150
|
+
local: {
|
|
151
|
+
tryBundle(outputDir: string): boolean {
|
|
152
|
+
if (packageMode) bundleFromPip(outputDir, options);
|
|
153
|
+
else bundleFromCheckout(outputDir, options);
|
|
154
|
+
return true;
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
command: packageMode
|
|
158
|
+
? ["bash", "-c", "echo 'Package-mode Python bundling runs locally (pip on the build host).' >&2; exit 1"]
|
|
159
|
+
: dockerCommand,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { resolveAmplifySecret } from "./amplify-secrets";
|
|
3
|
+
|
|
4
|
+
const URL_PATTERN = /https?:\/\/\S+/gi;
|
|
5
|
+
|
|
6
|
+
export async function slackSigningSecret(): Promise<string> {
|
|
7
|
+
return resolveAmplifySecret("PAPYRUS_SLACK_SIGNING_SECRET");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function allowedSlackUserIds(): Set<string> {
|
|
11
|
+
const raw = (process.env.PAPYRUS_SLACK_ALLOWED_USER_IDS ?? "").trim();
|
|
12
|
+
if (!raw) return new Set();
|
|
13
|
+
return new Set(raw.split(",").map((entry) => entry.trim()).filter(Boolean));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function verifySlackRequestSignature(input: {
|
|
17
|
+
signingSecret: string;
|
|
18
|
+
timestamp: string;
|
|
19
|
+
rawBody: Buffer;
|
|
20
|
+
signature: string;
|
|
21
|
+
maxAgeSeconds?: number;
|
|
22
|
+
}): boolean {
|
|
23
|
+
const secret = input.signingSecret.trim();
|
|
24
|
+
const timestamp = input.timestamp.trim();
|
|
25
|
+
const signature = input.signature.trim();
|
|
26
|
+
if (!secret || !timestamp || !signature) return false;
|
|
27
|
+
const requestTs = Number.parseInt(timestamp, 10);
|
|
28
|
+
if (!Number.isFinite(requestTs)) return false;
|
|
29
|
+
const maxAge = input.maxAgeSeconds ?? 60 * 5;
|
|
30
|
+
if (Math.abs(Math.floor(Date.now() / 1000) - requestTs) > maxAge) return false;
|
|
31
|
+
const base = `v0:${timestamp}:${input.rawBody.toString("utf8")}`;
|
|
32
|
+
const digest = createHmac("sha256", secret).update(base).digest("hex");
|
|
33
|
+
const expected = `v0=${digest}`;
|
|
34
|
+
const expectedBuffer = Buffer.from(expected);
|
|
35
|
+
const signatureBuffer = Buffer.from(signature);
|
|
36
|
+
if (expectedBuffer.length !== signatureBuffer.length) return false;
|
|
37
|
+
return timingSafeEqual(expectedBuffer, signatureBuffer);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function slackThreadId(teamId: string, channelId: string, threadTs: string): string {
|
|
41
|
+
const team = (teamId || "unknown").trim() || "unknown";
|
|
42
|
+
const channel = channelId.trim();
|
|
43
|
+
const root = threadTs.trim().replace(/\./g, "-");
|
|
44
|
+
return `thread-slack-${team}-${channel}-${root}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function chatMessageIdForSlackEvent(eventId: string): string {
|
|
48
|
+
const token = eventId.replace(/[^a-zA-Z0-9]/g, "").slice(0, 40) || "unknown";
|
|
49
|
+
return `message-console-slack-${token}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function shouldIgnoreSlackEvent(event: Record<string, unknown>): string | null {
|
|
53
|
+
if (event.bot_id || event.subtype === "bot_message" || event.subtype === "message_changed" || event.subtype === "message_deleted") {
|
|
54
|
+
return "bot-or-non-user-message";
|
|
55
|
+
}
|
|
56
|
+
const user = String(event.user ?? "").trim();
|
|
57
|
+
if (!user) return "missing-user";
|
|
58
|
+
const allowed = allowedSlackUserIds();
|
|
59
|
+
if (allowed.size > 0 && !allowed.has(user)) return "unauthorized-user";
|
|
60
|
+
const text = String(event.text ?? "").trim();
|
|
61
|
+
if (!text && !Array.isArray(event.files)) return "empty-message";
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function slackAgentInstructions(): string {
|
|
66
|
+
return [
|
|
67
|
+
"You are Papyrus, an editorial assistant for an autonomous newsroom, replying in Slack (not inbound email reference intake).",
|
|
68
|
+
"Use execute_tactus with the papyrus.* tool surface.",
|
|
69
|
+
"",
|
|
70
|
+
"Be concise, accurate, and concrete. Slack has no web UI: do not use papyrus.web.navigate, papyrus.web.current_location, or assume the user can see papyrus:// pages.",
|
|
71
|
+
"",
|
|
72
|
+
"Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable insight, recommend creating an insight Message instead of making every chat turn canonical knowledge.",
|
|
73
|
+
"",
|
|
74
|
+
"Respond openly to questions, commands, and discussion—the same conversational stance as the web console. Do not treat every message as a citation submission. Register references or create insights only when the user shares URLs/DOIs or asks you to file, summarize, or comment on specific material.",
|
|
75
|
+
"",
|
|
76
|
+
'For requests like "most recent references" or "tell me about recent references", do not ask clarifying questions first: immediately call execute_tactus with a Reference.list snippet, then summarize the results.',
|
|
77
|
+
"",
|
|
78
|
+
'When the user provides a URL and asks to add or register a reference, immediately call execute_tactus with Reference.create{ url = "<url>", apply = true } (not papyrus.reference.create).',
|
|
79
|
+
"",
|
|
80
|
+
"Keep Slack replies short; use bullet lists when listing references.",
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildSlackPrompt(input: {
|
|
85
|
+
event: Record<string, unknown>;
|
|
86
|
+
teamId: string | null;
|
|
87
|
+
channelId: string;
|
|
88
|
+
threadTs: string;
|
|
89
|
+
}): string {
|
|
90
|
+
const user = String(input.event.user ?? "slack-user").trim();
|
|
91
|
+
const text = String(input.event.text ?? "").trim();
|
|
92
|
+
const urls = text.match(URL_PATTERN) ?? [];
|
|
93
|
+
const lines = [
|
|
94
|
+
slackAgentInstructions(),
|
|
95
|
+
"",
|
|
96
|
+
`Slack team id: ${input.teamId ?? "(unknown)"}`,
|
|
97
|
+
`Slack channel id: ${input.channelId}`,
|
|
98
|
+
`Slack thread ts: ${input.threadTs}`,
|
|
99
|
+
`Slack user id: ${user}`,
|
|
100
|
+
"",
|
|
101
|
+
"User message:",
|
|
102
|
+
text || "(no text; see files metadata if present)",
|
|
103
|
+
];
|
|
104
|
+
if (Array.isArray(input.event.files) && input.event.files.length > 0) {
|
|
105
|
+
lines.push("", "Slack files JSON:", JSON.stringify(input.event.files, null, 2));
|
|
106
|
+
}
|
|
107
|
+
if (urls.length > 0) {
|
|
108
|
+
lines.push("", `Detected URL count: ${urls.length}`, "URLs:", ...urls.map((url) => `- ${url}`));
|
|
109
|
+
}
|
|
110
|
+
return lines.join("\n").trim();
|
|
111
|
+
}
|