@anthusai/papyrus 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/amplify/data/resource.js +70 -3
- package/amplify/data/resource.ts +75 -3
- package/amplify/data/schema.js +67 -0
- package/amplify/data/schema.ts +72 -0
- package/amplify/functions/category-action/handler.ts +190 -20
- package/amplify/functions/console-chat-responder/src/main.rs +1 -1
- package/amplify/functions/content-actions/handler.py +23 -0
- package/amplify/functions/content-actions/requirements.txt +1 -1
- package/amplify/functions/content-actions/resource.js +1 -1
- package/amplify/functions/content-actions/resource.ts +1 -1
- package/amplify/functions/renditions/requirements.txt +1 -1
- package/amplify/functions/shared/curation-delegation.ts +67 -0
- package/amplify/functions/shared/reference-curation-metadata.ts +20 -0
- package/amplify/functions/shared/slack-events.ts +2 -2
- package/amplify/publication/renditions-function.js +1 -1
- package/amplify/publication/renditions-function.ts +1 -1
- package/app/api/local-preview/derive/route.ts +28 -0
- package/app/api/local-preview/media/route.ts +25 -0
- package/app/globals.css +28 -28
- package/app/layout.tsx +8 -5
- package/app/newsroom/[section]/[...selection]/page.tsx +0 -1
- package/app/newsroom/[section]/page.tsx +0 -1
- package/app/newsroom/insights/[threadId]/page.tsx +5 -16
- package/app/newsroom/insights/page.tsx +4 -12
- package/components/administration-delegations-panel.tsx +204 -0
- package/components/board-card-excerpts.tsx +45 -0
- package/components/curation-delegation-client.ts +81 -0
- package/components/markus-ir-preview.tsx +47 -2
- package/components/news-desk-page.tsx +0 -1
- package/components/news-desk-taxonomy-client.ts +32 -220
- package/components/newsroom-article-editor.tsx +135 -39
- package/components/newsroom-assignment-detail.tsx +35 -1
- package/components/newsroom-board-view.tsx +29 -7
- package/components/newsroom-newsprint-board.css +9 -2
- package/components/newsroom-ops-overview.tsx +27 -5
- package/components/newsroom-organizer-shell.tsx +25 -7
- package/components/newsroom-organizer.css +218 -4
- package/components/newsroom-references-view.tsx +56 -21
- package/components/priming-proof-page.tsx +17 -8
- package/components/prospectus-account-chooser.tsx +98 -0
- package/components/reference-cyclotron-status.tsx +5 -5
- package/components/reference-cyclotron-suggestion.tsx +143 -0
- package/components/reference-live-agreement.tsx +62 -0
- package/components/story-reporting-trail-client.ts +91 -0
- package/components/story-reporting-trail.tsx +115 -0
- package/components/topic-steering-workspace.tsx +203 -674
- package/components/ui/sheet.tsx +1 -0
- package/components/wiki-concepts-view.tsx +153 -0
- package/components/wiki-index-client.ts +51 -0
- package/lib/content-actions-client.ts +3 -0
- package/lib/content-actions-demo.ts +55 -3
- package/lib/content-item-lifecycle.ts +4 -2
- package/lib/content-source-context.ts +9 -1
- package/lib/curation-delegation-helpers.ts +59 -0
- package/lib/cyclotron-suggestions.ts +263 -0
- package/lib/front-matter-fields.ts +42 -0
- package/lib/graphql-content-repository.ts +42 -7
- package/lib/local-preview-seed.ts +174 -3
- package/lib/local-preview-tenant.ts +27 -0
- package/lib/newsroom-articles.ts +2 -0
- package/lib/newsroom-board.ts +4 -0
- package/lib/newsroom-forum-routes.ts +2 -43
- package/lib/newsroom-index-filters.ts +3 -40
- package/lib/newsroom-media-backend.ts +5 -0
- package/lib/newsroom-nav.ts +1 -3
- package/lib/newsroom-organizer-nav.ts +5 -12
- package/lib/newsroom-section-routing.ts +1 -1
- package/lib/newsroom-sections.ts +12 -0
- package/lib/papyrus-web-locations.ts +3 -24
- package/lib/priming-proof-report.ts +1 -0
- package/lib/prospectus-tenant.ts +22 -0
- package/lib/relevance-decisions.ts +29 -0
- package/lib/semantic-graph.ts +5 -5
- package/lib/site-brand.ts +9 -0
- package/lib/story-reporting-trail.ts +106 -0
- package/lib/tenant-keys.ts +32 -0
- package/lib/wiki.ts +56 -0
- package/package.json +21 -1
- package/routes.manifest.json +23 -7
|
@@ -4,12 +4,25 @@ import { Amplify } from "aws-amplify";
|
|
|
4
4
|
import { generateClient } from "aws-amplify/data";
|
|
5
5
|
import type { Schema } from "../../data/resource";
|
|
6
6
|
import { putJsonModelPayload, putTextModelPayload, readJsonModelPayload } from "../shared/model-payloads";
|
|
7
|
-
import { referenceCurationMetadata } from "../shared/reference-curation-metadata";
|
|
7
|
+
import { curationActorKind, referenceCurationMetadata } from "../shared/reference-curation-metadata";
|
|
8
|
+
import {
|
|
9
|
+
CURATION_DELEGATION_OWNER_TYPE,
|
|
10
|
+
CURATION_DELEGATION_PAYLOAD_KIND,
|
|
11
|
+
delegationExpiry,
|
|
12
|
+
delegationRefusalMessage,
|
|
13
|
+
delegationVerdict,
|
|
14
|
+
newDelegationToken,
|
|
15
|
+
type CurationDelegation,
|
|
16
|
+
} from "../shared/curation-delegation";
|
|
8
17
|
|
|
9
18
|
type ReviewHandler = Schema["reviewSteeringProposal"]["functionHandler"];
|
|
10
19
|
type ReferenceCurationHandler = Schema["reviewReferenceCuration"]["functionHandler"];
|
|
20
|
+
type CreateCurationDelegationHandler = Schema["createCurationDelegation"]["functionHandler"];
|
|
21
|
+
type RevokeCurationDelegationHandler = Schema["revokeCurationDelegation"]["functionHandler"];
|
|
22
|
+
type ListCurationDelegationsHandler = Schema["listCurationDelegations"]["functionHandler"];
|
|
23
|
+
type RecordDelegatedCurationHandler = Schema["recordDelegatedCuration"]["functionHandler"];
|
|
11
24
|
type ReferenceQualityHandler = Schema["setReferenceQualityRating"]["functionHandler"];
|
|
12
|
-
type
|
|
25
|
+
type ReferenceNoteHandler = Schema["createReferenceNote"]["functionHandler"];
|
|
13
26
|
type MoveReferenceCorpusHandler = Schema["moveReferenceCorpus"]["functionHandler"];
|
|
14
27
|
type StartReferenceCurationHandler = Schema["startReferenceCuration"]["functionHandler"];
|
|
15
28
|
type GetReferenceCurationStatusHandler = Schema["getReferenceCurationStatus"]["functionHandler"];
|
|
@@ -23,8 +36,12 @@ type ReviewReferenceTopicLabelHandler = Schema["reviewReferenceTopicLabel"]["fun
|
|
|
23
36
|
type CategoryActionEvent =
|
|
24
37
|
| Parameters<ReviewHandler>[0]
|
|
25
38
|
| Parameters<ReferenceCurationHandler>[0]
|
|
39
|
+
| Parameters<CreateCurationDelegationHandler>[0]
|
|
40
|
+
| Parameters<RevokeCurationDelegationHandler>[0]
|
|
41
|
+
| Parameters<ListCurationDelegationsHandler>[0]
|
|
42
|
+
| Parameters<RecordDelegatedCurationHandler>[0]
|
|
26
43
|
| Parameters<ReferenceQualityHandler>[0]
|
|
27
|
-
| Parameters<
|
|
44
|
+
| Parameters<ReferenceNoteHandler>[0]
|
|
28
45
|
| Parameters<MoveReferenceCorpusHandler>[0]
|
|
29
46
|
| Parameters<StartReferenceCurationHandler>[0]
|
|
30
47
|
| Parameters<GetReferenceCurationStatusHandler>[0]
|
|
@@ -88,8 +105,12 @@ export const handler = async (event: CategoryActionEvent) => {
|
|
|
88
105
|
}
|
|
89
106
|
if (fieldName === "reviewSteeringProposal") return reviewSteeringProposal(event as Parameters<ReviewHandler>[0]);
|
|
90
107
|
if (fieldName === "reviewReferenceCuration") return reviewReferenceCuration(event as Parameters<ReferenceCurationHandler>[0]);
|
|
108
|
+
if (fieldName === "createCurationDelegation") return createCurationDelegation(event as Parameters<CreateCurationDelegationHandler>[0]);
|
|
109
|
+
if (fieldName === "revokeCurationDelegation") return revokeCurationDelegation(event as Parameters<RevokeCurationDelegationHandler>[0]);
|
|
110
|
+
if (fieldName === "listCurationDelegations") return listCurationDelegations(event as Parameters<ListCurationDelegationsHandler>[0]);
|
|
111
|
+
if (fieldName === "recordDelegatedCuration") return recordDelegatedCuration(event as Parameters<RecordDelegatedCurationHandler>[0]);
|
|
91
112
|
if (fieldName === "setReferenceQualityRating") return setReferenceQualityRating(event as Parameters<ReferenceQualityHandler>[0]);
|
|
92
|
-
if (fieldName === "
|
|
113
|
+
if (fieldName === "createReferenceNote") return createReferenceNote(event as Parameters<ReferenceNoteHandler>[0]);
|
|
93
114
|
if (fieldName === "moveReferenceCorpus") return moveReferenceCorpus(event as Parameters<MoveReferenceCorpusHandler>[0]);
|
|
94
115
|
if (fieldName === "startReferenceCuration") return startReferenceCuration(event as Parameters<StartReferenceCurationHandler>[0]);
|
|
95
116
|
if (fieldName === "getReferenceCurationStatus") return getReferenceCurationStatus(event as Parameters<GetReferenceCurationStatusHandler>[0]);
|
|
@@ -162,7 +183,10 @@ async function reviewSteeringProposal(event: Parameters<ReviewHandler>[0]) {
|
|
|
162
183
|
};
|
|
163
184
|
}
|
|
164
185
|
|
|
165
|
-
async function reviewReferenceCuration(
|
|
186
|
+
async function reviewReferenceCuration(
|
|
187
|
+
event: Parameters<ReferenceCurationHandler>[0],
|
|
188
|
+
delegation: { delegationId: string; relayedBy: string; instruction: string } | null = null,
|
|
189
|
+
) {
|
|
166
190
|
const client = await getDataClient();
|
|
167
191
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
168
192
|
const action = normalizeReferenceCurationAction(event.arguments.action);
|
|
@@ -234,6 +258,8 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
|
|
|
234
258
|
referenceLineageId,
|
|
235
259
|
decisionRelationId: normalizeOptionalString(event.arguments.decisionRelationId),
|
|
236
260
|
shareable: event.arguments.shareable ?? null,
|
|
261
|
+
actorKind: delegation ? "human" : curationActorKind(event.identity),
|
|
262
|
+
delegation,
|
|
237
263
|
}),
|
|
238
264
|
{ filename: "metadata.json", now },
|
|
239
265
|
);
|
|
@@ -300,6 +326,150 @@ async function reviewReferenceCuration(event: Parameters<ReferenceCurationHandle
|
|
|
300
326
|
};
|
|
301
327
|
}
|
|
302
328
|
|
|
329
|
+
function requireSignedInPerson(event: { identity?: unknown }): { sub: string; label: string; isAdmin: boolean } {
|
|
330
|
+
if (curationActorKind(event.identity) !== "human") {
|
|
331
|
+
throw new Error("Only a person signed in to the Newsroom can manage curation delegations.");
|
|
332
|
+
}
|
|
333
|
+
const identity = event.identity as { sub?: unknown; claims?: Record<string, unknown> };
|
|
334
|
+
const claims = identity.claims ?? {};
|
|
335
|
+
const groups = Array.isArray(claims["cognito:groups"]) ? (claims["cognito:groups"] as unknown[]) : [];
|
|
336
|
+
return {
|
|
337
|
+
sub: normalizeRequiredString(claims.sub ?? identity.sub, "identity.sub"),
|
|
338
|
+
label: normalizeOptionalString(claims.email) ?? normalizeOptionalString(claims.name) ?? "Newsroom editor",
|
|
339
|
+
isAdmin: groups.includes("admin"),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function readCurationDelegation(client: DataClient, delegationId: string): Promise<CurationDelegation | null> {
|
|
344
|
+
const stored = await readJsonModelPayload(client as any, "knowledgeRawPayload", delegationId, "raw_payload", CURATION_DELEGATION_PAYLOAD_KIND);
|
|
345
|
+
return stored ? (stored as unknown as CurationDelegation) : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function writeCurationDelegation(client: DataClient, delegation: CurationDelegation, now: string, created: boolean) {
|
|
349
|
+
const input = {
|
|
350
|
+
id: delegation.delegationId,
|
|
351
|
+
ownerType: CURATION_DELEGATION_OWNER_TYPE,
|
|
352
|
+
ownerId: delegation.grantorSub,
|
|
353
|
+
payloadKind: CURATION_DELEGATION_PAYLOAD_KIND,
|
|
354
|
+
createdAt: delegation.createdAt,
|
|
355
|
+
updatedAt: now,
|
|
356
|
+
};
|
|
357
|
+
if (created) {
|
|
358
|
+
await requireDataResult(client.models.KnowledgeRawPayload.create(input), "create curation delegation");
|
|
359
|
+
} else {
|
|
360
|
+
await requireDataResult(client.models.KnowledgeRawPayload.update(input), "update curation delegation");
|
|
361
|
+
}
|
|
362
|
+
await putJsonModelPayload(
|
|
363
|
+
client as any,
|
|
364
|
+
{ ownerKind: "knowledgeRawPayload", ownerId: delegation.delegationId },
|
|
365
|
+
"raw_payload",
|
|
366
|
+
CURATION_DELEGATION_PAYLOAD_KIND,
|
|
367
|
+
delegation as unknown as Record<string, unknown>,
|
|
368
|
+
{ filename: "curation-delegation.json", now },
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function delegationResult(delegation: CurationDelegation, token: string | null) {
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
delegationId: delegation.delegationId,
|
|
376
|
+
token,
|
|
377
|
+
agentLabel: delegation.agentLabel,
|
|
378
|
+
grantorLabel: delegation.grantorLabel,
|
|
379
|
+
expiresAt: delegation.expiresAt,
|
|
380
|
+
revokedAt: delegation.revokedAt,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function createCurationDelegation(event: Parameters<CreateCurationDelegationHandler>[0]) {
|
|
385
|
+
const person = requireSignedInPerson(event);
|
|
386
|
+
const client = await getDataClient();
|
|
387
|
+
const now = new Date().toISOString();
|
|
388
|
+
const delegationId = `curation-delegation-${randomUUID()}`;
|
|
389
|
+
const { token, tokenHash } = newDelegationToken(delegationId);
|
|
390
|
+
const delegation: CurationDelegation = {
|
|
391
|
+
delegationId,
|
|
392
|
+
tokenHash,
|
|
393
|
+
grantorSub: person.sub,
|
|
394
|
+
grantorLabel: person.label,
|
|
395
|
+
agentLabel: normalizeRequiredString(event.arguments.agentLabel, "agentLabel"),
|
|
396
|
+
createdAt: now,
|
|
397
|
+
expiresAt: delegationExpiry(now, event.arguments.hours),
|
|
398
|
+
revokedAt: null,
|
|
399
|
+
};
|
|
400
|
+
await writeCurationDelegation(client, delegation, now, true);
|
|
401
|
+
return delegationResult(delegation, token);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
async function revokeCurationDelegation(event: Parameters<RevokeCurationDelegationHandler>[0]) {
|
|
405
|
+
const person = requireSignedInPerson(event);
|
|
406
|
+
const client = await getDataClient();
|
|
407
|
+
const delegation = await readCurationDelegation(client, normalizeRequiredString(event.arguments.delegationId, "delegationId"));
|
|
408
|
+
if (!delegation) throw new Error("That delegation was not found.");
|
|
409
|
+
if (delegation.grantorSub !== person.sub && !person.isAdmin) throw new Error("Only the person who granted a delegation can revoke it.");
|
|
410
|
+
const now = new Date().toISOString();
|
|
411
|
+
const revoked = { ...delegation, revokedAt: delegation.revokedAt ?? now };
|
|
412
|
+
await writeCurationDelegation(client, revoked, now, false);
|
|
413
|
+
return delegationResult(revoked, null);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async function listCurationDelegations(event: Parameters<ListCurationDelegationsHandler>[0]) {
|
|
417
|
+
const person = requireSignedInPerson(event);
|
|
418
|
+
const client = await getDataClient();
|
|
419
|
+
const delegations: CurationDelegation[] = [];
|
|
420
|
+
let nextToken: string | null | undefined;
|
|
421
|
+
do {
|
|
422
|
+
const page: any = await (client.models.KnowledgeRawPayload as any).listKnowledgeRawPayloadsByOwnerAndKind(
|
|
423
|
+
{ ownerId: person.sub, payloadKind: { eq: CURATION_DELEGATION_PAYLOAD_KIND } },
|
|
424
|
+
{ nextToken },
|
|
425
|
+
);
|
|
426
|
+
assertNoDataErrors(page.errors, "list curation delegations");
|
|
427
|
+
for (const payload of page.data ?? []) {
|
|
428
|
+
const delegation = await readCurationDelegation(client, payload.id);
|
|
429
|
+
if (delegation) delegations.push(delegation);
|
|
430
|
+
}
|
|
431
|
+
nextToken = page.nextToken;
|
|
432
|
+
} while (nextToken);
|
|
433
|
+
return delegations
|
|
434
|
+
.sort((first, second) => second.createdAt.localeCompare(first.createdAt))
|
|
435
|
+
.map((delegation) => delegationResult(delegation, null));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function recordDelegatedCuration(event: Parameters<RecordDelegatedCurationHandler>[0]) {
|
|
439
|
+
const client = await getDataClient();
|
|
440
|
+
const now = new Date().toISOString();
|
|
441
|
+
const delegationId = normalizeRequiredString(event.arguments.delegationId, "delegationId");
|
|
442
|
+
const agentLabel = normalizeRequiredString(event.arguments.agentLabel, "agentLabel");
|
|
443
|
+
const delegation = await readCurationDelegation(client, delegationId);
|
|
444
|
+
const verdict = delegationVerdict(delegation, {
|
|
445
|
+
token: normalizeRequiredString(event.arguments.delegationToken, "delegationToken"),
|
|
446
|
+
agentLabel,
|
|
447
|
+
now,
|
|
448
|
+
});
|
|
449
|
+
if (!verdict.ok || !delegation) {
|
|
450
|
+
throw new Error(delegationRefusalMessage(verdict.ok ? "unknown-token" : verdict.reason));
|
|
451
|
+
}
|
|
452
|
+
const instruction = normalizeRequiredString(event.arguments.instruction, "instruction").slice(0, 2000);
|
|
453
|
+
const namedPerson = normalizeRequiredString(event.arguments.onBehalfOf, "onBehalfOf");
|
|
454
|
+
if (namedPerson.toLowerCase() !== delegation.grantorLabel.toLowerCase()) {
|
|
455
|
+
throw new Error(`That delegation was granted by ${delegation.grantorLabel}, not ${namedPerson}.`);
|
|
456
|
+
}
|
|
457
|
+
const onBehalfOfPerson = {
|
|
458
|
+
...event,
|
|
459
|
+
arguments: {
|
|
460
|
+
referenceId: event.arguments.referenceId,
|
|
461
|
+
action: event.arguments.action,
|
|
462
|
+
note: event.arguments.note,
|
|
463
|
+
reasonCode: event.arguments.reasonCode,
|
|
464
|
+
decisionRelationId: event.arguments.decisionRelationId,
|
|
465
|
+
shareable: event.arguments.shareable,
|
|
466
|
+
actorSub: delegation.grantorSub,
|
|
467
|
+
actorLabel: delegation.grantorLabel,
|
|
468
|
+
},
|
|
469
|
+
} as unknown as Parameters<ReferenceCurationHandler>[0];
|
|
470
|
+
return reviewReferenceCuration(onBehalfOfPerson, { delegationId, relayedBy: agentLabel, instruction });
|
|
471
|
+
}
|
|
472
|
+
|
|
303
473
|
async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandler>[0]) {
|
|
304
474
|
const client = await getDataClient();
|
|
305
475
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
@@ -403,7 +573,7 @@ async function setReferenceQualityRating(event: Parameters<ReferenceQualityHandl
|
|
|
403
573
|
};
|
|
404
574
|
}
|
|
405
575
|
|
|
406
|
-
async function
|
|
576
|
+
async function createReferenceNote(event: Parameters<ReferenceNoteHandler>[0]) {
|
|
407
577
|
const client = await getDataClient();
|
|
408
578
|
const referenceId = normalizeRequiredString(event.arguments.referenceId, "referenceId");
|
|
409
579
|
const summary = normalizeRequiredString(event.arguments.summary, "summary");
|
|
@@ -414,10 +584,10 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
414
584
|
const actorLabel = normalizeOptionalString(event.arguments.actorLabel) ?? getIdentityLabel(event);
|
|
415
585
|
const actor = actorLabel ?? actorSub ?? "Papyrus newsroom";
|
|
416
586
|
const referenceLineageId = normalizeOptionalString(reference.lineageId) ?? referenceId;
|
|
417
|
-
const messageId = `message-reference-
|
|
587
|
+
const messageId = `message-reference-note-${safeId(referenceLineageId)}-${now.replace(/[^0-9TZ]/g, "")}-${randomUUID().slice(0, 8)}`;
|
|
418
588
|
const relationId = `semantic-relation-${hashStable([
|
|
419
589
|
`message#${messageId}`,
|
|
420
|
-
"
|
|
590
|
+
"note_about",
|
|
421
591
|
`reference#${referenceId}`,
|
|
422
592
|
]).slice(0, 24)}`;
|
|
423
593
|
|
|
@@ -425,7 +595,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
425
595
|
client.models.Message.create({
|
|
426
596
|
...buildCanonicalMessageInput({
|
|
427
597
|
id: messageId,
|
|
428
|
-
messageKind: "
|
|
598
|
+
messageKind: "note",
|
|
429
599
|
messageDomain: "knowledge",
|
|
430
600
|
summary,
|
|
431
601
|
source: "newsroom",
|
|
@@ -436,7 +606,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
436
606
|
threadId: messageId,
|
|
437
607
|
sequenceNumber: 1,
|
|
438
608
|
}),
|
|
439
|
-
"create
|
|
609
|
+
"create note Message",
|
|
440
610
|
);
|
|
441
611
|
await putTextModelPayload(
|
|
442
612
|
client as any,
|
|
@@ -452,7 +622,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
452
622
|
"metadata",
|
|
453
623
|
"metadata",
|
|
454
624
|
{
|
|
455
|
-
kind: "reference.
|
|
625
|
+
kind: "reference.note.created",
|
|
456
626
|
targetKind: "reference",
|
|
457
627
|
targetId: referenceId,
|
|
458
628
|
targetLineageId: referenceLineageId,
|
|
@@ -463,8 +633,8 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
463
633
|
client.models.SemanticRelation.create({
|
|
464
634
|
id: relationId,
|
|
465
635
|
relationState: "current",
|
|
466
|
-
predicate: "
|
|
467
|
-
...semanticRelationTypeFieldsForPredicate("
|
|
636
|
+
predicate: "note_about",
|
|
637
|
+
...semanticRelationTypeFieldsForPredicate("note_about"),
|
|
468
638
|
subjectKind: "message",
|
|
469
639
|
subjectId: messageId,
|
|
470
640
|
subjectLineageId: messageId,
|
|
@@ -476,7 +646,7 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
476
646
|
subjectStateKey: semanticStateKey("message", messageId),
|
|
477
647
|
objectStateKey: semanticStateKey("reference", referenceLineageId),
|
|
478
648
|
objectSubjectStateKey: `${semanticStateKey("reference", referenceLineageId)}#message`,
|
|
479
|
-
predicateObjectStateKey: `
|
|
649
|
+
predicateObjectStateKey: `note_about#${semanticStateKey("reference", referenceLineageId)}`,
|
|
480
650
|
subjectVersionKey: semanticVersionKey("message", messageId),
|
|
481
651
|
objectVersionKey: semanticVersionKey("reference", referenceId),
|
|
482
652
|
score: 1,
|
|
@@ -492,17 +662,17 @@ async function createReferenceInsight(event: Parameters<ReferenceInsightHandler>
|
|
|
492
662
|
updatedAt: now,
|
|
493
663
|
newsroomFeedKey: "semanticRelations",
|
|
494
664
|
metadata: JSON.stringify({
|
|
495
|
-
kind: "reference.
|
|
665
|
+
kind: "reference.note",
|
|
496
666
|
summary,
|
|
497
667
|
actor,
|
|
498
668
|
}),
|
|
499
669
|
}),
|
|
500
|
-
"create
|
|
670
|
+
"create note_about relation",
|
|
501
671
|
);
|
|
502
672
|
await updateNewsroomSummaryForSemanticRelationDelta(
|
|
503
673
|
client,
|
|
504
674
|
{
|
|
505
|
-
relationTypeKey: "
|
|
675
|
+
relationTypeKey: "note_about",
|
|
506
676
|
relationDomain: "commentary",
|
|
507
677
|
subjectKind: "message",
|
|
508
678
|
objectKind: "reference",
|
|
@@ -1497,10 +1667,10 @@ function semanticRelationTypeFieldsForPredicate(predicate: string): {
|
|
|
1497
1667
|
relationDomain: "curation",
|
|
1498
1668
|
};
|
|
1499
1669
|
}
|
|
1500
|
-
if (predicate === "
|
|
1670
|
+
if (predicate === "note_about") {
|
|
1501
1671
|
return {
|
|
1502
|
-
relationTypeId: "semantic-relation-type-
|
|
1503
|
-
relationTypeKey: "
|
|
1672
|
+
relationTypeId: "semantic-relation-type-note-about",
|
|
1673
|
+
relationTypeKey: "note_about",
|
|
1504
1674
|
relationDomain: "knowledge",
|
|
1505
1675
|
};
|
|
1506
1676
|
}
|
|
@@ -2190,7 +2190,7 @@ fn build_openai_messages(
|
|
|
2190
2190
|
) -> Vec<Value> {
|
|
2191
2191
|
let mut messages = vec![json!({
|
|
2192
2192
|
"role": "system",
|
|
2193
|
-
"content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable
|
|
2193
|
+
"content": "You are Papyrus, an editorial assistant for an autonomous newsroom. Be concise, accurate, and concrete. Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge. Use execute_tactus for Papyrus runtime work. Distinguish references from assignments: Reference.* is for scholarly sources; Assignment.* is for newsroom work items (research, curation, intake). When the user provides a URL and asks to add, register, or file a reference, immediately call Reference.create{ url = \"<url>\", title = \"<optional>\", apply = true }—do not use Reference.register, papyrus.reference.create, or Assignment.create unless they asked for research work. For \"most recent references\" or similar, immediately call Reference.list{ limit = <count>, order = \"newest\" } and summarize. For \"last/latest/most recent research assignment\" (or \"research assignment\" without a specific id), immediately call Assignment.list{ type = \"research\", limit = 1 } (includes research.edition-candidate, research.tavily-deep, and other research.* types), then Assignment.get{ id = \"<that id>\" } if the user wants detail—do not use Reference.list and do not list all assignment types (curation.reference-intake is not a research assignment). To create research work, use Assignment.create{ type = \"research\", title = \"...\", apply = true }."
|
|
2194
2194
|
})];
|
|
2195
2195
|
if !static_prompt.publication_mission.trim().is_empty() {
|
|
2196
2196
|
messages.push(json!({
|
|
@@ -22,6 +22,7 @@ from papyrus_content.rebuild_trigger import (
|
|
|
22
22
|
trigger_rebuild,
|
|
23
23
|
trigger_staging_build,
|
|
24
24
|
)
|
|
25
|
+
from papyrus_content.story_reporting import save_story_version
|
|
25
26
|
from papyrus_content.publishing import (
|
|
26
27
|
ItemFields,
|
|
27
28
|
PublishError,
|
|
@@ -188,6 +189,28 @@ def run_projection_task(task: dict) -> dict:
|
|
|
188
189
|
|
|
189
190
|
def save_item_draft_action(arguments: dict, actor: str) -> dict:
|
|
190
191
|
client, _claims = create_authoring_client()
|
|
192
|
+
if arguments.get("newVersion") and arguments.get("id"):
|
|
193
|
+
existing = client.get_record("Item", arguments["id"])
|
|
194
|
+
if existing is None:
|
|
195
|
+
raise PublishError("not-found", "That story was not found.")
|
|
196
|
+
record = save_story_version(
|
|
197
|
+
client,
|
|
198
|
+
existing.get("lineageId") or existing["id"],
|
|
199
|
+
front_matter_yaml=arguments.get("frontMatterYaml"),
|
|
200
|
+
body_markus=arguments.get("bodyMarkus") or "",
|
|
201
|
+
actor=actor,
|
|
202
|
+
change_reason=arguments.get("changeReason") or "edited in the newsroom",
|
|
203
|
+
)
|
|
204
|
+
return {
|
|
205
|
+
"ok": True,
|
|
206
|
+
"item": {
|
|
207
|
+
"id": record["id"],
|
|
208
|
+
"contentHash": record["contentHash"],
|
|
209
|
+
"status": record["status"],
|
|
210
|
+
"slug": record["slug"],
|
|
211
|
+
"versionNumber": record.get("versionNumber"),
|
|
212
|
+
},
|
|
213
|
+
}
|
|
191
214
|
fields = ItemFields(
|
|
192
215
|
type=arguments["type"],
|
|
193
216
|
slug=arguments["slug"],
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
8
8
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
10
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
10
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
|
|
11
11
|
function installCheckoutRequirements(outputDir) {
|
|
12
12
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
13
13
|
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
|
|
@@ -10,7 +10,7 @@ import { pythonLambdaCode, type SiteBundleFile } from "../shared/python-bundle";
|
|
|
10
10
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const projectRoot = path.resolve(dirname, "../../..");
|
|
12
12
|
|
|
13
|
-
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
13
|
+
export const contentActionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12", "awscrt>=0.23,<1"];
|
|
14
14
|
|
|
15
15
|
function installCheckoutRequirements(outputDir: string): void {
|
|
16
16
|
const requirementsPath = path.join(projectRoot, "amplify/functions/content-actions/requirements.txt");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto"
|
|
2
|
+
|
|
3
|
+
export const CURATION_DELEGATION_PAYLOAD_KIND = "curation-delegation"
|
|
4
|
+
export const CURATION_DELEGATION_OWNER_TYPE = "curationDelegation"
|
|
5
|
+
export const DEFAULT_DELEGATION_HOURS = 8
|
|
6
|
+
export const MAXIMUM_DELEGATION_HOURS = 72
|
|
7
|
+
|
|
8
|
+
export type CurationDelegation = {
|
|
9
|
+
delegationId: string
|
|
10
|
+
tokenHash: string
|
|
11
|
+
grantorSub: string
|
|
12
|
+
grantorLabel: string
|
|
13
|
+
agentLabel: string
|
|
14
|
+
createdAt: string
|
|
15
|
+
expiresAt: string
|
|
16
|
+
revokedAt: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type DelegationVerdict =
|
|
20
|
+
| { ok: true }
|
|
21
|
+
| { ok: false; reason: "unknown-token" | "wrong-agent" | "revoked" | "expired" }
|
|
22
|
+
|
|
23
|
+
export function hashDelegationToken(token: string): string {
|
|
24
|
+
return createHash("sha256").update(token, "utf8").digest("hex")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function newDelegationToken(delegationId: string): { token: string; tokenHash: string } {
|
|
28
|
+
const token = `${delegationId}~${randomBytes(24).toString("base64url")}`
|
|
29
|
+
return { token, tokenHash: hashDelegationToken(token) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function delegationIdFromToken(token: string): string | null {
|
|
33
|
+
const separator = token.indexOf("~")
|
|
34
|
+
return separator > 0 ? token.slice(0, separator) : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function delegationExpiry(now: string, hours: number | null | undefined): string {
|
|
38
|
+
const requested = hours ?? DEFAULT_DELEGATION_HOURS
|
|
39
|
+
if (!Number.isFinite(requested) || requested <= 0 || requested > MAXIMUM_DELEGATION_HOURS) {
|
|
40
|
+
throw new Error(`A delegation lasts between 1 and ${MAXIMUM_DELEGATION_HOURS} hours.`)
|
|
41
|
+
}
|
|
42
|
+
return new Date(Date.parse(now) + requested * 3_600_000).toISOString()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function delegationVerdict(
|
|
46
|
+
delegation: CurationDelegation | null,
|
|
47
|
+
presented: { token: string; agentLabel: string; now: string },
|
|
48
|
+
): DelegationVerdict {
|
|
49
|
+
if (!delegation || delegation.tokenHash !== hashDelegationToken(presented.token)) {
|
|
50
|
+
return { ok: false, reason: "unknown-token" }
|
|
51
|
+
}
|
|
52
|
+
if (delegation.agentLabel.trim().toLowerCase() !== presented.agentLabel.trim().toLowerCase()) {
|
|
53
|
+
return { ok: false, reason: "wrong-agent" }
|
|
54
|
+
}
|
|
55
|
+
if (delegation.revokedAt) return { ok: false, reason: "revoked" }
|
|
56
|
+
if (Date.parse(delegation.expiresAt) <= Date.parse(presented.now)) return { ok: false, reason: "expired" }
|
|
57
|
+
return { ok: true }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function delegationRefusalMessage(reason: Exclude<DelegationVerdict, { ok: true }>["reason"]): string {
|
|
61
|
+
return {
|
|
62
|
+
"unknown-token": "That delegation token is not recognized.",
|
|
63
|
+
"wrong-agent": "That delegation was granted to a different agent.",
|
|
64
|
+
"revoked": "That delegation was revoked.",
|
|
65
|
+
"expired": "That delegation has expired. Ask the person for a new one.",
|
|
66
|
+
}[reason]
|
|
67
|
+
}
|
|
@@ -12,6 +12,21 @@ export type ReferenceCurationMetadataInput = {
|
|
|
12
12
|
referenceLineageId: string
|
|
13
13
|
decisionRelationId?: string | null
|
|
14
14
|
shareable?: boolean | null
|
|
15
|
+
actorKind?: CurationActorKind | null
|
|
16
|
+
delegation?: { delegationId: string; relayedBy: string; instruction: string } | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CurationActorKind = "human" | "service"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A curation is human only when the caller signed in through the user pool, so
|
|
23
|
+
* AppSync supplied a verified subject claim. Request arguments never decide it;
|
|
24
|
+
* IAM-signed service callers and agents have no such claim.
|
|
25
|
+
*/
|
|
26
|
+
export function curationActorKind(identity: unknown): CurationActorKind {
|
|
27
|
+
const claims = (identity as {claims?: Record<string, unknown>} | null | undefined)?.claims
|
|
28
|
+
const subject = claims?.sub
|
|
29
|
+
return typeof subject === "string" && subject.trim() !== "" ? "human" : "service"
|
|
15
30
|
}
|
|
16
31
|
|
|
17
32
|
export function referenceCurationMetadata(input: ReferenceCurationMetadataInput): Record<string, unknown> {
|
|
@@ -23,6 +38,11 @@ export function referenceCurationMetadata(input: ReferenceCurationMetadataInput)
|
|
|
23
38
|
curationReasonCode: input.reasonCode,
|
|
24
39
|
referenceId: input.referenceId,
|
|
25
40
|
referenceLineageId: input.referenceLineageId,
|
|
41
|
+
...(input.actorKind ? {actorKind: input.actorKind} : {}),
|
|
42
|
+
...(input.delegation
|
|
43
|
+
? {attestation: "delegated", relayedBy: input.delegation.relayedBy, delegationId: input.delegation.delegationId,
|
|
44
|
+
instruction: input.delegation.instruction}
|
|
45
|
+
: {}),
|
|
26
46
|
...(decisionRelationId ? {decisionRelationId} : {}),
|
|
27
47
|
...(decisionRelationId ? {shareable: input.shareable === true} : {}),
|
|
28
48
|
}
|
|
@@ -69,9 +69,9 @@ export function slackAgentInstructions(): string {
|
|
|
69
69
|
"",
|
|
70
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
71
|
"",
|
|
72
|
-
"Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable
|
|
72
|
+
"Raw console chat turns are working memory and are excluded from default semantic searches unless explicitly requested. When a chat produces durable note, recommend creating a note Message instead of making every chat turn canonical knowledge.",
|
|
73
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
|
|
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 notes only when the user shares URLs/DOIs or asks you to file, summarize, or comment on specific material.",
|
|
75
75
|
"",
|
|
76
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
77
|
"",
|
|
@@ -11,7 +11,7 @@ import { pythonLambdaCode } from "../functions/shared/python-bundle.js";
|
|
|
11
11
|
import { RENDITIONS_EPHEMERAL_STORAGE_MB, RENDITIONS_MEDIA_PREFIX, RENDITIONS_MEMORY_MB, RENDITIONS_TIMEOUT_SECONDS, renditionsEnvironment, renditionsEventPattern, } from "./renditions-settings.js";
|
|
12
12
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const projectRoot = path.resolve(dirname, "../..");
|
|
14
|
-
export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
14
|
+
export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12"];
|
|
15
15
|
function installCheckoutRequirements(outputDir) {
|
|
16
16
|
const requirementsPath = path.join(projectRoot, "amplify/functions/renditions/requirements.txt");
|
|
17
17
|
execSync(`python3 -m pip install -r "${requirementsPath}" -t "${outputDir}" --no-cache-dir --platform manylinux2014_aarch64 --implementation cp --python-version 3.12 --only-binary=:all: --ignore-requires-python`, { cwd: projectRoot, stdio: "inherit" });
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
23
|
const projectRoot = path.resolve(dirname, "../..");
|
|
24
24
|
|
|
25
|
-
export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.
|
|
25
|
+
export const renditionsRequirements = ["PyYAML>=6.0.2,<7", "citeproc-py>=0.8.2,<1", "anthus-markus==0.7.0", "Pillow>=11.3,<12"];
|
|
26
26
|
|
|
27
27
|
export type RenditionsFunctionProps = {
|
|
28
28
|
siteBucket: s3.IBucket;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
4
|
+
|
|
5
|
+
export const dynamic = "force-dynamic";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Local preview only: derives a story's Markus body through the real Python engine so directives render.
|
|
9
|
+
* Set PAPYRUS_PREVIEW_PYTHON to a Python that has Papyrus and its Markus extra installed.
|
|
10
|
+
*/
|
|
11
|
+
export async function POST(request: Request) {
|
|
12
|
+
if (process.env.NEXT_PUBLIC_PAPYRUS_LOCAL_PREVIEW !== "1") return new NextResponse(null, { status: 404 });
|
|
13
|
+
const python = process.env.PAPYRUS_PREVIEW_PYTHON?.trim();
|
|
14
|
+
if (!python) return NextResponse.json({ error: "PAPYRUS_PREVIEW_PYTHON is not set." }, { status: 501 });
|
|
15
|
+
const input = await request.text();
|
|
16
|
+
const script = path.join(process.cwd(), "scripts", "local-preview-derive.py");
|
|
17
|
+
const output = await new Promise<string>((resolve, reject) => {
|
|
18
|
+
const child = spawn(python, [script], { env: process.env });
|
|
19
|
+
let stdout = "";
|
|
20
|
+
let stderr = "";
|
|
21
|
+
child.stdout.on("data", chunk => { stdout += chunk; });
|
|
22
|
+
child.stderr.on("data", chunk => { stderr += chunk; });
|
|
23
|
+
child.on("error", reject);
|
|
24
|
+
child.on("close", code => code === 0 ? resolve(stdout) : reject(new Error(stderr || `derive exited with ${code}`)));
|
|
25
|
+
child.stdin.end(input);
|
|
26
|
+
}).catch((reason: unknown) => JSON.stringify({ error: reason instanceof Error ? reason.message : "derive failed" }));
|
|
27
|
+
return new NextResponse(output, { headers: { "content-type": "application/json" } });
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
const CONTENT_TYPES: Record<string, string> = { jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", avif: "image/avif" };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Local preview only: streams one story image from the publication's storage bucket so the previews show real figures.
|
|
10
|
+
* Set PAPYRUS_PREVIEW_MEDIA_BUCKET to the bucket; the server's AWS credentials do the reading.
|
|
11
|
+
*/
|
|
12
|
+
export async function GET(request: Request) {
|
|
13
|
+
if (process.env.NEXT_PUBLIC_PAPYRUS_LOCAL_PREVIEW !== "1") return new NextResponse(null, { status: 404 });
|
|
14
|
+
const bucket = process.env.PAPYRUS_PREVIEW_MEDIA_BUCKET?.trim();
|
|
15
|
+
const key = new URL(request.url).searchParams.get("key") ?? "";
|
|
16
|
+
if (!bucket) return NextResponse.json({ error: "PAPYRUS_PREVIEW_MEDIA_BUCKET is not set." }, { status: 501 });
|
|
17
|
+
if (!/^media\/[A-Za-z0-9_\-./]+$/.test(key) || key.includes("..")) return new NextResponse(null, { status: 400 });
|
|
18
|
+
const extension = key.split(".").pop()?.toLowerCase() ?? "";
|
|
19
|
+
const child = spawn("aws", ["s3", "cp", `s3://${bucket}/${key}`, "-"], { env: process.env });
|
|
20
|
+
const chunks: Buffer[] = [];
|
|
21
|
+
child.stdout.on("data", chunk => chunks.push(chunk));
|
|
22
|
+
const exitCode = await new Promise<number>(resolve => { child.on("close", code => resolve(code ?? 1)); child.on("error", () => resolve(1)); });
|
|
23
|
+
if (exitCode !== 0) return new NextResponse(null, { status: 404 });
|
|
24
|
+
return new NextResponse(Buffer.concat(chunks), { headers: { "content-type": CONTENT_TYPES[extension] ?? "application/octet-stream", "cache-control": "private, max-age=300" } });
|
|
25
|
+
}
|