@anthusai/papyrus 0.0.0 → 1.0.0-next.1
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 +2037 -0
- package/amplify/data/resource.ts +2148 -0
- package/amplify/data/schema.js +1850 -0
- package/amplify/data/schema.ts +1953 -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/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 +115 -0
- package/amplify/functions/shared/python-bundle.ts +141 -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/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +30 -0
- package/amplify/site-backend.js +587 -0
- package/amplify/site-backend.ts +704 -0
- package/amplify/storage/resource.js +18 -0
- package/amplify/storage/resource.ts +19 -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 +10329 -0
- package/app/layout.tsx +146 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -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/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +10 -0
- package/bin/papyrus-infra.mjs +39 -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 +47 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +119 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +319 -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/topic-steering-workspace.tsx +18063 -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 +109 -0
- package/infra/amplify-app-shell.ts +155 -0
- package/infra/index.js +12 -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 +89 -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-load-timing.ts +23 -0
- package/lib/content-repository.ts +40 -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/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +986 -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-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-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-nav.ts +42 -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 +41 -0
- package/lib/site-brand.ts +179 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/middleware.ts +78 -0
- package/package.json +296 -1
- package/publications/anth_us/brand.ts +68 -0
- package/publications/anth_us/theme.css +66 -0
- package/publications/papyrus/theme.css +11 -0
- package/publications/pilobol_us/brand.ts +37 -0
- package/publications/pilobol_us/theme.css +140 -0
- package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
- package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
- package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
- package/publications/threat_intelligence/brand.ts +40 -0
- package/publications/threat_intelligence/pictograms/art.tsx +608 -0
- package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
- package/publications/threat_intelligence/pictograms/registry.ts +30 -0
- package/publications/threat_intelligence/pictograms/system.tsx +305 -0
- package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
- package/publications/threat_intelligence/theme.css +1440 -0
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +174 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +477 -0
- package/routes.manifest.json +182 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -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 +47 -0
|
@@ -0,0 +1,1316 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { generateClient } from "aws-amplify/data";
|
|
4
|
+
import type { Schema } from "../amplify/data/resource";
|
|
5
|
+
import { configureAmplifyClient } from "../components/amplify-client-provider";
|
|
6
|
+
import { loadModelPayloadsForOwner, uploadModelPayloadForOwner } from "../components/news-desk-taxonomy-client";
|
|
7
|
+
|
|
8
|
+
const USER_POOL_AUTH_MODE = "userPool";
|
|
9
|
+
const API_KEY_AUTH_MODE = "apiKey";
|
|
10
|
+
const DEFAULT_CONSOLE_THREAD_ANCHOR_KEY = "site#papyrus";
|
|
11
|
+
const CONSOLE_NEWSROOM_FEED_KEY = "consoleChat";
|
|
12
|
+
const CONSOLE_MESSAGE_KIND = "console_chat_turn";
|
|
13
|
+
const CONSOLE_MESSAGE_DOMAIN = "conversation";
|
|
14
|
+
const CONSOLE_TOOL_CALL_KIND = "console_tool_call";
|
|
15
|
+
const CONSOLE_TOOL_RESULT_KIND = "console_tool_result";
|
|
16
|
+
|
|
17
|
+
const CONSOLE_THREAD_FIELDS = `
|
|
18
|
+
id
|
|
19
|
+
threadKind
|
|
20
|
+
status
|
|
21
|
+
title
|
|
22
|
+
summary
|
|
23
|
+
primaryAnchorKind
|
|
24
|
+
primaryAnchorId
|
|
25
|
+
primaryAnchorLineageId
|
|
26
|
+
primaryAnchorKey
|
|
27
|
+
createdByLabel
|
|
28
|
+
messageCount
|
|
29
|
+
lastMessageId
|
|
30
|
+
lastMessageAt
|
|
31
|
+
contextDigest
|
|
32
|
+
metadata
|
|
33
|
+
createdAt
|
|
34
|
+
updatedAt
|
|
35
|
+
newsroomFeedKey
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const CONSOLE_MESSAGE_FIELDS = `
|
|
39
|
+
id
|
|
40
|
+
threadId
|
|
41
|
+
parentMessageId
|
|
42
|
+
sequenceNumber
|
|
43
|
+
role
|
|
44
|
+
messageKind
|
|
45
|
+
messageType
|
|
46
|
+
content
|
|
47
|
+
summary
|
|
48
|
+
responseStatus
|
|
49
|
+
responseOwner
|
|
50
|
+
responseStartedAt
|
|
51
|
+
responseCompletedAt
|
|
52
|
+
responseError
|
|
53
|
+
authorUserProfileId
|
|
54
|
+
metadata
|
|
55
|
+
createdAt
|
|
56
|
+
updatedAt
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const CONSOLE_MESSAGE_FIELDS_LEGACY = `
|
|
60
|
+
id
|
|
61
|
+
messageKind
|
|
62
|
+
messageDomain
|
|
63
|
+
summary
|
|
64
|
+
source
|
|
65
|
+
importRunId
|
|
66
|
+
authorSub
|
|
67
|
+
authorUserProfileId
|
|
68
|
+
authorLabel
|
|
69
|
+
createdAt
|
|
70
|
+
updatedAt
|
|
71
|
+
newsroomFeedKey
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const CONSOLE_MESSAGE_SELECTION_CANDIDATES = [
|
|
75
|
+
"id",
|
|
76
|
+
"threadId",
|
|
77
|
+
"parentMessageId",
|
|
78
|
+
"sequenceNumber",
|
|
79
|
+
"role",
|
|
80
|
+
"messageKind",
|
|
81
|
+
"messageType",
|
|
82
|
+
"content",
|
|
83
|
+
"summary",
|
|
84
|
+
"responseStatus",
|
|
85
|
+
"responseOwner",
|
|
86
|
+
"responseStartedAt",
|
|
87
|
+
"responseCompletedAt",
|
|
88
|
+
"responseError",
|
|
89
|
+
"messageDomain",
|
|
90
|
+
"source",
|
|
91
|
+
"importRunId",
|
|
92
|
+
"authorSub",
|
|
93
|
+
"authorUserProfileId",
|
|
94
|
+
"authorLabel",
|
|
95
|
+
"newsroomFeedKey",
|
|
96
|
+
"metadata",
|
|
97
|
+
"body",
|
|
98
|
+
"createdAt",
|
|
99
|
+
"updatedAt",
|
|
100
|
+
] as const;
|
|
101
|
+
|
|
102
|
+
const CONSOLE_MESSAGE_REQUIRED_FIELDS = ["id", "createdAt", "updatedAt", "summary"] as const;
|
|
103
|
+
|
|
104
|
+
const CONSOLE_SCHEMA_INTROSPECTION = `
|
|
105
|
+
query ConsoleChatSchemaIntrospection {
|
|
106
|
+
queryType: __type(name: "Query") {
|
|
107
|
+
fields {
|
|
108
|
+
name
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
messageType: __type(name: "Message") {
|
|
112
|
+
fields {
|
|
113
|
+
name
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
createMessageInputType: __type(name: "CreateMessageInput") {
|
|
117
|
+
inputFields {
|
|
118
|
+
name
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
|
|
124
|
+
const LIST_CONSOLE_THREADS = `
|
|
125
|
+
query ListMessageThreads(
|
|
126
|
+
$limit: Int
|
|
127
|
+
$nextToken: String
|
|
128
|
+
) {
|
|
129
|
+
listMessageThreads(
|
|
130
|
+
limit: $limit
|
|
131
|
+
nextToken: $nextToken
|
|
132
|
+
) {
|
|
133
|
+
items {
|
|
134
|
+
${CONSOLE_THREAD_FIELDS}
|
|
135
|
+
}
|
|
136
|
+
nextToken
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
`;
|
|
140
|
+
|
|
141
|
+
const LIST_CONSOLE_MESSAGES_LEGACY = `
|
|
142
|
+
query ListMessages(
|
|
143
|
+
$limit: Int
|
|
144
|
+
$nextToken: String
|
|
145
|
+
) {
|
|
146
|
+
listMessages(
|
|
147
|
+
limit: $limit
|
|
148
|
+
nextToken: $nextToken
|
|
149
|
+
) {
|
|
150
|
+
items {
|
|
151
|
+
${CONSOLE_MESSAGE_FIELDS_LEGACY}
|
|
152
|
+
}
|
|
153
|
+
nextToken
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const LIST_CONSOLE_MESSAGES_BY_FEED_LEGACY = `
|
|
159
|
+
query ListMessagesByNewsroomFeedAndCreatedAt(
|
|
160
|
+
$newsroomFeedKey: String!
|
|
161
|
+
$limit: Int
|
|
162
|
+
$nextToken: String
|
|
163
|
+
) {
|
|
164
|
+
listMessagesByNewsroomFeedAndCreatedAt(
|
|
165
|
+
newsroomFeedKey: $newsroomFeedKey
|
|
166
|
+
limit: $limit
|
|
167
|
+
nextToken: $nextToken
|
|
168
|
+
) {
|
|
169
|
+
items {
|
|
170
|
+
${CONSOLE_MESSAGE_FIELDS_LEGACY}
|
|
171
|
+
}
|
|
172
|
+
nextToken
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
const CREATE_CONSOLE_THREAD = `
|
|
178
|
+
mutation CreateMessageThread($input: CreateMessageThreadInput!) {
|
|
179
|
+
createMessageThread(input: $input) {
|
|
180
|
+
${CONSOLE_THREAD_FIELDS}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
const UPDATE_CONSOLE_THREAD = `
|
|
186
|
+
mutation UpdateMessageThread($input: UpdateMessageThreadInput!) {
|
|
187
|
+
updateMessageThread(input: $input) {
|
|
188
|
+
${CONSOLE_THREAD_FIELDS}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
const CREATE_CONSOLE_MESSAGE_LEGACY = `
|
|
194
|
+
mutation CreateMessage($input: CreateMessageInput!) {
|
|
195
|
+
createMessage(input: $input) {
|
|
196
|
+
${CONSOLE_MESSAGE_FIELDS_LEGACY}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
export type ConsoleChatThread = {
|
|
202
|
+
id: string;
|
|
203
|
+
threadKind: string;
|
|
204
|
+
status: string;
|
|
205
|
+
title: string;
|
|
206
|
+
summary?: string | null;
|
|
207
|
+
primaryAnchorKind?: string | null;
|
|
208
|
+
primaryAnchorId?: string | null;
|
|
209
|
+
primaryAnchorLineageId?: string | null;
|
|
210
|
+
primaryAnchorKey?: string | null;
|
|
211
|
+
createdByLabel?: string | null;
|
|
212
|
+
messageCount?: number | null;
|
|
213
|
+
lastMessageId?: string | null;
|
|
214
|
+
lastMessageAt?: string | null;
|
|
215
|
+
activeResponseMessageId?: string | null;
|
|
216
|
+
contextDigest?: string | null;
|
|
217
|
+
metadata?: unknown;
|
|
218
|
+
createdAt: string;
|
|
219
|
+
updatedAt: string;
|
|
220
|
+
newsroomFeedKey?: string | null;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export type ConsoleChatMessage = {
|
|
224
|
+
id: string;
|
|
225
|
+
threadId?: string | null;
|
|
226
|
+
parentMessageId?: string | null;
|
|
227
|
+
sequenceNumber?: number | null;
|
|
228
|
+
role?: string | null;
|
|
229
|
+
messageKind?: string | null;
|
|
230
|
+
messageType?: string | null;
|
|
231
|
+
content?: string | null;
|
|
232
|
+
summary?: string | null;
|
|
233
|
+
responseStatus?: string | null;
|
|
234
|
+
responseOwner?: string | null;
|
|
235
|
+
responseStartedAt?: string | null;
|
|
236
|
+
responseCompletedAt?: string | null;
|
|
237
|
+
responseError?: string | null;
|
|
238
|
+
source?: string | null;
|
|
239
|
+
authorUserProfileId?: string | null;
|
|
240
|
+
authorLabel?: string | null;
|
|
241
|
+
metadata?: unknown;
|
|
242
|
+
createdAt: string;
|
|
243
|
+
updatedAt?: string | null;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
type ConsoleMessageSubscriptionInput = {
|
|
247
|
+
filter?: {
|
|
248
|
+
threadId?: {
|
|
249
|
+
eq?: string;
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
type ConsoleMessageSubscription = {
|
|
255
|
+
unsubscribe: () => void;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
type ConsoleMessageSubscriptionModel = {
|
|
259
|
+
onCreate: (input?: ConsoleMessageSubscriptionInput) => {
|
|
260
|
+
subscribe: (observer: {
|
|
261
|
+
next: (value: unknown) => void;
|
|
262
|
+
error?: (error: unknown) => void;
|
|
263
|
+
}) => ConsoleMessageSubscription;
|
|
264
|
+
};
|
|
265
|
+
onUpdate: (input?: ConsoleMessageSubscriptionInput) => {
|
|
266
|
+
subscribe: (observer: {
|
|
267
|
+
next: (value: unknown) => void;
|
|
268
|
+
error?: (error: unknown) => void;
|
|
269
|
+
}) => ConsoleMessageSubscription;
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
type GraphQLClient = {
|
|
274
|
+
graphql: (options: {
|
|
275
|
+
query: string;
|
|
276
|
+
variables?: Record<string, unknown>;
|
|
277
|
+
authMode?: typeof USER_POOL_AUTH_MODE | typeof API_KEY_AUTH_MODE;
|
|
278
|
+
}) => Promise<{
|
|
279
|
+
data?: Record<string, unknown> | null;
|
|
280
|
+
errors?: Array<{ message?: string | null; errorType?: string | null } | string | null> | null;
|
|
281
|
+
}>;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
type ConsoleSchemaSnapshot = {
|
|
285
|
+
queryFields: Set<string>;
|
|
286
|
+
messageFields: Set<string>;
|
|
287
|
+
createMessageInputFields: Set<string>;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
let consoleSchemaSnapshot: ConsoleSchemaSnapshot | null = null;
|
|
291
|
+
let consoleSchemaSnapshotPromise: Promise<ConsoleSchemaSnapshot> | null = null;
|
|
292
|
+
|
|
293
|
+
function getGraphQLClient(): GraphQLClient {
|
|
294
|
+
configureAmplifyClient();
|
|
295
|
+
return generateClient<Schema>() as unknown as GraphQLClient;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function runGraphQLWithAuthModes(options: {
|
|
299
|
+
query: string;
|
|
300
|
+
variables?: Record<string, unknown>;
|
|
301
|
+
}): Promise<{
|
|
302
|
+
data?: Record<string, unknown> | null;
|
|
303
|
+
errors?: Array<{ message?: string | null; errorType?: string | null } | string | null> | null;
|
|
304
|
+
}> {
|
|
305
|
+
const client = getGraphQLClient();
|
|
306
|
+
try {
|
|
307
|
+
return await client.graphql({
|
|
308
|
+
query: options.query,
|
|
309
|
+
variables: options.variables,
|
|
310
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
311
|
+
});
|
|
312
|
+
} catch (firstError) {
|
|
313
|
+
try {
|
|
314
|
+
return await client.graphql({
|
|
315
|
+
query: options.query,
|
|
316
|
+
variables: options.variables,
|
|
317
|
+
authMode: API_KEY_AUTH_MODE,
|
|
318
|
+
});
|
|
319
|
+
} catch (secondError) {
|
|
320
|
+
throw normalizeUnknownError(secondError, "graphql", firstError);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function formatGraphQLError(error: { message?: string | null; errorType?: string | null } | string | null): string {
|
|
326
|
+
if (typeof error === "string") return error;
|
|
327
|
+
if (!error) return "Unknown GraphQL error";
|
|
328
|
+
return [error.errorType, error.message].filter(Boolean).join(": ") || "Unknown GraphQL error";
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function assertGraphQL<T>(response: {
|
|
332
|
+
data?: Record<string, unknown> | null;
|
|
333
|
+
errors?: Array<{ message?: string | null; errorType?: string | null } | string | null> | null;
|
|
334
|
+
}, field: string, options?: { allowPartialData?: boolean }): T {
|
|
335
|
+
const value = response.data?.[field];
|
|
336
|
+
if (response.errors?.length) {
|
|
337
|
+
if (options?.allowPartialData && value !== undefined && value !== null) {
|
|
338
|
+
return value as T;
|
|
339
|
+
}
|
|
340
|
+
throw new Error(response.errors.map(formatGraphQLError).join("; "));
|
|
341
|
+
}
|
|
342
|
+
if (value === undefined || value === null) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Console chat API is unavailable (${field}). Regenerate amplify_outputs.json from the current backend or deploy the MessageThread schema.`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return value as T;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function readConnectionItems<T>(value: unknown): T[] {
|
|
351
|
+
if (!value || typeof value !== "object") return [];
|
|
352
|
+
const items = (value as { items?: unknown }).items;
|
|
353
|
+
return Array.isArray(items) ? items as T[] : [];
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function readConnectionNextToken(value: unknown): string | null {
|
|
357
|
+
if (!value || typeof value !== "object") return null;
|
|
358
|
+
const nextToken = (value as { nextToken?: unknown }).nextToken;
|
|
359
|
+
return typeof nextToken === "string" && nextToken.trim() ? nextToken : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function readNameSet(value: unknown): Set<string> {
|
|
363
|
+
const names = new Set<string>();
|
|
364
|
+
if (!value || typeof value !== "object") return names;
|
|
365
|
+
const fields = (value as { fields?: unknown; inputFields?: unknown }).fields
|
|
366
|
+
?? (value as { inputFields?: unknown }).inputFields;
|
|
367
|
+
if (!Array.isArray(fields)) return names;
|
|
368
|
+
for (const field of fields) {
|
|
369
|
+
if (!field || typeof field !== "object") continue;
|
|
370
|
+
const name = (field as { name?: unknown }).name;
|
|
371
|
+
if (typeof name === "string" && name.trim()) names.add(name.trim());
|
|
372
|
+
}
|
|
373
|
+
return names;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function chooseMessageSelectionSet(schemaFields: Set<string> | null): string {
|
|
377
|
+
if (!schemaFields || !schemaFields.size) return CONSOLE_MESSAGE_FIELDS;
|
|
378
|
+
const selected = CONSOLE_MESSAGE_SELECTION_CANDIDATES.filter((field) => schemaFields.has(field));
|
|
379
|
+
for (const field of CONSOLE_MESSAGE_REQUIRED_FIELDS) {
|
|
380
|
+
if (schemaFields.has(field) && !selected.includes(field)) selected.push(field);
|
|
381
|
+
}
|
|
382
|
+
if (!selected.includes("id")) selected.unshift("id");
|
|
383
|
+
return selected.join("\n ");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function fieldValuesForCreateMessageInput(input: CreateConsoleMessageInput): Record<string, unknown> {
|
|
387
|
+
return {
|
|
388
|
+
id: input.id,
|
|
389
|
+
messageKind: input.messageKind,
|
|
390
|
+
messageDomain: input.messageDomain,
|
|
391
|
+
status: input.status,
|
|
392
|
+
summary: input.summary ?? truncateSummary(input.content ?? null),
|
|
393
|
+
source: input.source,
|
|
394
|
+
importRunId: null,
|
|
395
|
+
authorSub: null,
|
|
396
|
+
authorUserProfileId: input.authorUserProfileId ?? null,
|
|
397
|
+
authorLabel: input.authorLabel,
|
|
398
|
+
threadId: input.threadId ?? null,
|
|
399
|
+
parentMessageId: input.parentMessageId ?? null,
|
|
400
|
+
sequenceNumber: input.sequenceNumber ?? null,
|
|
401
|
+
role: input.role ?? null,
|
|
402
|
+
messageType: input.messageType ?? null,
|
|
403
|
+
semanticLayer: input.semanticLayer,
|
|
404
|
+
searchVisibility: input.searchVisibility,
|
|
405
|
+
responseTarget: input.responseTarget,
|
|
406
|
+
responseStatus: input.responseStatus ?? null,
|
|
407
|
+
responseOwner: input.responseOwner ?? null,
|
|
408
|
+
responseStartedAt: input.responseStartedAt ?? null,
|
|
409
|
+
responseCompletedAt: input.responseCompletedAt ?? null,
|
|
410
|
+
responseError: input.responseError ?? null,
|
|
411
|
+
metadata: input.metadata,
|
|
412
|
+
createdAt: input.createdAt,
|
|
413
|
+
updatedAt: input.updatedAt,
|
|
414
|
+
newsroomFeedKey: input.newsroomFeedKey,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function buildCreateMessageInputCandidates(
|
|
419
|
+
input: CreateConsoleMessageInput,
|
|
420
|
+
availableFields: Set<string>,
|
|
421
|
+
): Record<string, unknown>[] {
|
|
422
|
+
const values = fieldValuesForCreateMessageInput(input);
|
|
423
|
+
const candidates: Record<string, unknown>[] = [];
|
|
424
|
+
const seen = new Set<string>();
|
|
425
|
+
|
|
426
|
+
const pushCandidate = (candidate: Record<string, unknown>) => {
|
|
427
|
+
const cleaned: Record<string, unknown> = {};
|
|
428
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
429
|
+
if (value !== undefined) cleaned[key] = value;
|
|
430
|
+
}
|
|
431
|
+
const signature = JSON.stringify(Object.keys(cleaned).sort().map((key) => [key, cleaned[key]]));
|
|
432
|
+
if (!seen.has(signature)) {
|
|
433
|
+
seen.add(signature);
|
|
434
|
+
candidates.push(cleaned);
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
if (availableFields.size) {
|
|
439
|
+
pushCandidate(sanitizeCreateMessageInput(input, availableFields));
|
|
440
|
+
} else {
|
|
441
|
+
pushCandidate(values);
|
|
442
|
+
pushCandidate({
|
|
443
|
+
id: values.id,
|
|
444
|
+
messageKind: values.messageKind,
|
|
445
|
+
messageDomain: values.messageDomain,
|
|
446
|
+
status: values.status,
|
|
447
|
+
summary: values.summary,
|
|
448
|
+
source: values.source,
|
|
449
|
+
authorLabel: values.authorLabel,
|
|
450
|
+
createdAt: values.createdAt,
|
|
451
|
+
updatedAt: values.updatedAt,
|
|
452
|
+
newsroomFeedKey: values.newsroomFeedKey,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return candidates;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function loadConsoleSchemaSnapshot(): Promise<ConsoleSchemaSnapshot> {
|
|
460
|
+
if (consoleSchemaSnapshot) return consoleSchemaSnapshot;
|
|
461
|
+
if (consoleSchemaSnapshotPromise) return consoleSchemaSnapshotPromise;
|
|
462
|
+
consoleSchemaSnapshotPromise = (async () => {
|
|
463
|
+
try {
|
|
464
|
+
const response = await runGraphQLWithAuthModes({
|
|
465
|
+
query: CONSOLE_SCHEMA_INTROSPECTION,
|
|
466
|
+
});
|
|
467
|
+
if (response.errors?.length) throw new Error(response.errors.map(formatGraphQLError).join("; "));
|
|
468
|
+
const data = response.data ?? {};
|
|
469
|
+
const snapshot: ConsoleSchemaSnapshot = {
|
|
470
|
+
queryFields: readNameSet(data.queryType),
|
|
471
|
+
messageFields: readNameSet(data.messageType),
|
|
472
|
+
createMessageInputFields: readNameSet(data.createMessageInputType),
|
|
473
|
+
};
|
|
474
|
+
consoleSchemaSnapshot = snapshot;
|
|
475
|
+
return snapshot;
|
|
476
|
+
} catch {
|
|
477
|
+
const snapshot: ConsoleSchemaSnapshot = {
|
|
478
|
+
queryFields: new Set<string>(),
|
|
479
|
+
messageFields: new Set<string>(),
|
|
480
|
+
createMessageInputFields: new Set<string>(),
|
|
481
|
+
};
|
|
482
|
+
consoleSchemaSnapshot = snapshot;
|
|
483
|
+
return snapshot;
|
|
484
|
+
} finally {
|
|
485
|
+
consoleSchemaSnapshotPromise = null;
|
|
486
|
+
}
|
|
487
|
+
})();
|
|
488
|
+
return consoleSchemaSnapshotPromise;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function buildListMessagesQuery(
|
|
492
|
+
field: "listMessages" | "listMessagesByNewsroomFeedAndCreatedAt" | "listMessagesByKindAndCreatedAt" | "listMessagesByDomainAndCreatedAt",
|
|
493
|
+
selectionSet: string,
|
|
494
|
+
): string {
|
|
495
|
+
if (field === "listMessages") {
|
|
496
|
+
return `
|
|
497
|
+
query ListMessages($limit: Int, $nextToken: String) {
|
|
498
|
+
listMessages(limit: $limit, nextToken: $nextToken) {
|
|
499
|
+
items {
|
|
500
|
+
${selectionSet}
|
|
501
|
+
}
|
|
502
|
+
nextToken
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
`;
|
|
506
|
+
}
|
|
507
|
+
if (field === "listMessagesByNewsroomFeedAndCreatedAt") {
|
|
508
|
+
return `
|
|
509
|
+
query ListMessagesByNewsroomFeedAndCreatedAt($newsroomFeedKey: String!, $limit: Int, $nextToken: String) {
|
|
510
|
+
listMessagesByNewsroomFeedAndCreatedAt(newsroomFeedKey: $newsroomFeedKey, limit: $limit, nextToken: $nextToken) {
|
|
511
|
+
items {
|
|
512
|
+
${selectionSet}
|
|
513
|
+
}
|
|
514
|
+
nextToken
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
`;
|
|
518
|
+
}
|
|
519
|
+
if (field === "listMessagesByKindAndCreatedAt") {
|
|
520
|
+
return `
|
|
521
|
+
query ListMessagesByKindAndCreatedAt($messageKind: String!, $limit: Int, $nextToken: String) {
|
|
522
|
+
listMessagesByKindAndCreatedAt(messageKind: $messageKind, limit: $limit, nextToken: $nextToken) {
|
|
523
|
+
items {
|
|
524
|
+
${selectionSet}
|
|
525
|
+
}
|
|
526
|
+
nextToken
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
`;
|
|
530
|
+
}
|
|
531
|
+
return `
|
|
532
|
+
query ListMessagesByDomainAndCreatedAt($messageDomain: String!, $limit: Int, $nextToken: String) {
|
|
533
|
+
listMessagesByDomainAndCreatedAt(messageDomain: $messageDomain, limit: $limit, nextToken: $nextToken) {
|
|
534
|
+
items {
|
|
535
|
+
${selectionSet}
|
|
536
|
+
}
|
|
537
|
+
nextToken
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
`;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function sanitizeCreateMessageInput(
|
|
544
|
+
input: CreateConsoleMessageInput,
|
|
545
|
+
availableFields: Set<string>,
|
|
546
|
+
): Record<string, unknown> {
|
|
547
|
+
const fieldValues: Record<string, unknown> = {
|
|
548
|
+
id: input.id,
|
|
549
|
+
messageKind: input.messageKind,
|
|
550
|
+
messageDomain: input.messageDomain,
|
|
551
|
+
status: input.status,
|
|
552
|
+
summary: input.summary ?? truncateSummary(input.content ?? null),
|
|
553
|
+
source: input.source,
|
|
554
|
+
importRunId: null,
|
|
555
|
+
authorSub: null,
|
|
556
|
+
authorUserProfileId: input.authorUserProfileId ?? null,
|
|
557
|
+
authorLabel: input.authorLabel,
|
|
558
|
+
threadId: input.threadId ?? null,
|
|
559
|
+
parentMessageId: input.parentMessageId ?? null,
|
|
560
|
+
sequenceNumber: input.sequenceNumber ?? null,
|
|
561
|
+
role: input.role ?? null,
|
|
562
|
+
messageType: input.messageType ?? null,
|
|
563
|
+
semanticLayer: input.semanticLayer,
|
|
564
|
+
searchVisibility: input.searchVisibility,
|
|
565
|
+
responseTarget: input.responseTarget,
|
|
566
|
+
responseStatus: input.responseStatus ?? null,
|
|
567
|
+
responseOwner: input.responseOwner ?? null,
|
|
568
|
+
responseStartedAt: input.responseStartedAt ?? null,
|
|
569
|
+
responseCompletedAt: input.responseCompletedAt ?? null,
|
|
570
|
+
responseError: input.responseError ?? null,
|
|
571
|
+
metadata: input.metadata,
|
|
572
|
+
createdAt: input.createdAt,
|
|
573
|
+
updatedAt: input.updatedAt,
|
|
574
|
+
newsroomFeedKey: input.newsroomFeedKey,
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
if (!availableFields.size) {
|
|
578
|
+
const conservativeFields = [
|
|
579
|
+
"id",
|
|
580
|
+
"messageKind",
|
|
581
|
+
"messageDomain",
|
|
582
|
+
"status",
|
|
583
|
+
"summary",
|
|
584
|
+
"source",
|
|
585
|
+
"authorLabel",
|
|
586
|
+
"createdAt",
|
|
587
|
+
"updatedAt",
|
|
588
|
+
"newsroomFeedKey",
|
|
589
|
+
] as const;
|
|
590
|
+
const conservative: Record<string, unknown> = {};
|
|
591
|
+
for (const key of conservativeFields) {
|
|
592
|
+
const value = fieldValues[key];
|
|
593
|
+
if (value !== undefined) conservative[key] = value;
|
|
594
|
+
}
|
|
595
|
+
return conservative;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const sanitized: Record<string, unknown> = {};
|
|
599
|
+
for (const [key, value] of Object.entries(fieldValues)) {
|
|
600
|
+
if (!availableFields.has(key)) continue;
|
|
601
|
+
if (value === undefined) continue;
|
|
602
|
+
sanitized[key] = value;
|
|
603
|
+
}
|
|
604
|
+
return sanitized;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function truncateSummary(value: string | null): string | null {
|
|
608
|
+
if (!value) return null;
|
|
609
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
610
|
+
if (!normalized) return null;
|
|
611
|
+
return normalized.length > 180 ? `${normalized.slice(0, 179)}…` : normalized;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function maybeUploadConsoleMessageBody(messageId: string, content: string | null | undefined): Promise<void> {
|
|
615
|
+
const text = String(content ?? "").trim();
|
|
616
|
+
if (!text) return;
|
|
617
|
+
await uploadModelPayloadForOwner({
|
|
618
|
+
ownerKind: "message",
|
|
619
|
+
ownerId: messageId,
|
|
620
|
+
ownerLineageId: messageId,
|
|
621
|
+
role: "message_body",
|
|
622
|
+
sortKey: "message",
|
|
623
|
+
filename: "message.md",
|
|
624
|
+
mediaType: "text/markdown",
|
|
625
|
+
content: text.endsWith("\n") ? text : `${text}\n`,
|
|
626
|
+
status: "active",
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
export async function listConsoleThreads(primaryAnchorKeyOrLimit: string | number = DEFAULT_CONSOLE_THREAD_ANCHOR_KEY, limit = 25): Promise<ConsoleChatThread[]> {
|
|
631
|
+
const primaryAnchorKey =
|
|
632
|
+
typeof primaryAnchorKeyOrLimit === "string" ? primaryAnchorKeyOrLimit : DEFAULT_CONSOLE_THREAD_ANCHOR_KEY;
|
|
633
|
+
const resolvedLimit =
|
|
634
|
+
typeof primaryAnchorKeyOrLimit === "number" ? primaryAnchorKeyOrLimit : limit;
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const maxThreads = Math.max(resolvedLimit * 2, 100);
|
|
638
|
+
const threads = await listConsoleThreadsViaThreadModel(maxThreads);
|
|
639
|
+
return threads
|
|
640
|
+
.filter((thread) => thread.threadKind === "console" && (thread.primaryAnchorKey ?? DEFAULT_CONSOLE_THREAD_ANCHOR_KEY) === primaryAnchorKey)
|
|
641
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
642
|
+
.slice(0, resolvedLimit);
|
|
643
|
+
} catch (error) {
|
|
644
|
+
if (!isUnavailableQueryError(error)) throw error;
|
|
645
|
+
const messages = await listConsoleMessagesRaw(Math.max(resolvedLimit * 20, 500), 5000);
|
|
646
|
+
return deriveConsoleThreadsFromMessages(messages, primaryAnchorKey).slice(0, resolvedLimit);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export async function listConsoleThreadByAnchor(primaryAnchorKey: string): Promise<ConsoleChatThread | null> {
|
|
651
|
+
return (await listConsoleThreads(primaryAnchorKey, 1))[0] ?? null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export async function listConsoleMessagesByThread(threadId: string): Promise<ConsoleChatMessage[]> {
|
|
655
|
+
const messages = await listConsoleMessagesRaw(300, 5000);
|
|
656
|
+
const hasExplicitThreadIds = messages.some((message) => typeof message.threadId === "string" && message.threadId.trim());
|
|
657
|
+
const scoped = hasExplicitThreadIds
|
|
658
|
+
? messages.filter((message) => message.threadId === threadId)
|
|
659
|
+
: messages;
|
|
660
|
+
return scoped.sort(
|
|
661
|
+
(left, right) =>
|
|
662
|
+
(left.sequenceNumber ?? 0) - (right.sequenceNumber ?? 0) ||
|
|
663
|
+
left.createdAt.localeCompare(right.createdAt),
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export async function createConsoleThread(input: ConsoleChatThread): Promise<ConsoleChatThread> {
|
|
668
|
+
try {
|
|
669
|
+
const response = await getGraphQLClient().graphql({
|
|
670
|
+
query: CREATE_CONSOLE_THREAD,
|
|
671
|
+
variables: { input },
|
|
672
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
673
|
+
});
|
|
674
|
+
return assertGraphQL<ConsoleChatThread>(response, "createMessageThread");
|
|
675
|
+
} catch (error) {
|
|
676
|
+
if (isUnavailableQueryError(error)) return input;
|
|
677
|
+
throw normalizeUnknownError(error, "createMessageThread");
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
export async function updateConsoleThread(input: Pick<ConsoleChatThread, "id"> & Partial<ConsoleChatThread>): Promise<ConsoleChatThread> {
|
|
682
|
+
try {
|
|
683
|
+
const response = await getGraphQLClient().graphql({
|
|
684
|
+
query: UPDATE_CONSOLE_THREAD,
|
|
685
|
+
variables: { input },
|
|
686
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
687
|
+
});
|
|
688
|
+
return assertGraphQL<ConsoleChatThread>(response, "updateMessageThread");
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (isUnavailableQueryError(error)) {
|
|
691
|
+
const now = new Date().toISOString();
|
|
692
|
+
return {
|
|
693
|
+
id: input.id,
|
|
694
|
+
threadKind: input.threadKind ?? "console",
|
|
695
|
+
status: input.status ?? "active",
|
|
696
|
+
title: input.title ?? "Papyrus Console",
|
|
697
|
+
summary: input.summary ?? null,
|
|
698
|
+
primaryAnchorKind: input.primaryAnchorKind ?? "site",
|
|
699
|
+
primaryAnchorId: input.primaryAnchorId ?? "papyrus",
|
|
700
|
+
primaryAnchorLineageId: input.primaryAnchorLineageId ?? "papyrus",
|
|
701
|
+
primaryAnchorKey: input.primaryAnchorKey ?? DEFAULT_CONSOLE_THREAD_ANCHOR_KEY,
|
|
702
|
+
createdByLabel: input.createdByLabel ?? null,
|
|
703
|
+
messageCount: input.messageCount ?? null,
|
|
704
|
+
lastMessageId: input.lastMessageId ?? null,
|
|
705
|
+
lastMessageAt: input.lastMessageAt ?? null,
|
|
706
|
+
activeResponseMessageId: input.activeResponseMessageId ?? null,
|
|
707
|
+
contextDigest: input.contextDigest ?? null,
|
|
708
|
+
metadata: input.metadata ?? null,
|
|
709
|
+
createdAt: input.createdAt ?? now,
|
|
710
|
+
updatedAt: input.updatedAt ?? now,
|
|
711
|
+
newsroomFeedKey: input.newsroomFeedKey ?? CONSOLE_NEWSROOM_FEED_KEY,
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
throw normalizeUnknownError(error, "updateMessageThread");
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export type CreateConsoleMessageInput = ConsoleChatMessage & {
|
|
719
|
+
messageDomain: string;
|
|
720
|
+
status: string;
|
|
721
|
+
source: string;
|
|
722
|
+
authorLabel: string;
|
|
723
|
+
semanticLayer: string;
|
|
724
|
+
searchVisibility: string;
|
|
725
|
+
responseTarget: string;
|
|
726
|
+
metadata: string;
|
|
727
|
+
updatedAt: string;
|
|
728
|
+
newsroomFeedKey: string;
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
export async function createConsoleMessage(input: CreateConsoleMessageInput): Promise<ConsoleChatMessage> {
|
|
732
|
+
const snapshot = await loadConsoleSchemaSnapshot();
|
|
733
|
+
const selectionSets = snapshot.messageFields.size
|
|
734
|
+
? [chooseMessageSelectionSet(snapshot.messageFields)]
|
|
735
|
+
: [CONSOLE_MESSAGE_FIELDS, CONSOLE_MESSAGE_FIELDS_LEGACY];
|
|
736
|
+
const inputCandidates = buildCreateMessageInputCandidates(input, snapshot.createMessageInputFields);
|
|
737
|
+
let lastError: unknown = null;
|
|
738
|
+
|
|
739
|
+
for (const selectionSet of selectionSets) {
|
|
740
|
+
const createMutation = `
|
|
741
|
+
mutation CreateMessage($input: CreateMessageInput!) {
|
|
742
|
+
createMessage(input: $input) {
|
|
743
|
+
${selectionSet}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
`;
|
|
747
|
+
for (const candidateInput of inputCandidates) {
|
|
748
|
+
try {
|
|
749
|
+
const response = await getGraphQLClient().graphql({
|
|
750
|
+
query: createMutation,
|
|
751
|
+
variables: { input: candidateInput },
|
|
752
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
753
|
+
});
|
|
754
|
+
const created = normalizeConsoleMessageRecord(assertGraphQL<Record<string, unknown>>(response, "createMessage"));
|
|
755
|
+
await maybeUploadConsoleMessageBody(created.id, input.content);
|
|
756
|
+
return input.content?.trim() ? { ...created, content: input.content } : created;
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (!isUnavailableQueryError(error)) throw normalizeUnknownError(error, "createMessage");
|
|
759
|
+
lastError = error;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (lastError) {
|
|
765
|
+
try {
|
|
766
|
+
const response = await getGraphQLClient().graphql({
|
|
767
|
+
query: CREATE_CONSOLE_MESSAGE_LEGACY,
|
|
768
|
+
variables: {
|
|
769
|
+
input: inputCandidates[inputCandidates.length - 1] ?? {},
|
|
770
|
+
},
|
|
771
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
772
|
+
});
|
|
773
|
+
const created = normalizeConsoleMessageRecord(assertGraphQL<Record<string, unknown>>(response, "createMessage"));
|
|
774
|
+
await maybeUploadConsoleMessageBody(created.id, input.content);
|
|
775
|
+
return input.content?.trim() ? { ...created, content: input.content } : created;
|
|
776
|
+
} catch (error) {
|
|
777
|
+
if (!isUnavailableQueryError(error)) throw normalizeUnknownError(error, "createMessage");
|
|
778
|
+
lastError = error;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
throw normalizeUnknownError(lastError, "createMessage");
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export async function updateConsoleMessage(
|
|
786
|
+
input: Pick<ConsoleChatMessage, "id"> & Partial<ConsoleChatMessage>,
|
|
787
|
+
): Promise<ConsoleChatMessage> {
|
|
788
|
+
const now = new Date().toISOString();
|
|
789
|
+
const normalizedInput: Record<string, unknown> = {};
|
|
790
|
+
for (const [key, value] of Object.entries(input)) {
|
|
791
|
+
if (value !== undefined) normalizedInput[key] = value;
|
|
792
|
+
}
|
|
793
|
+
if (!normalizedInput.updatedAt) normalizedInput.updatedAt = now;
|
|
794
|
+
const snapshot = await loadConsoleSchemaSnapshot();
|
|
795
|
+
const selectionSets = snapshot.messageFields.size
|
|
796
|
+
? [chooseMessageSelectionSet(snapshot.messageFields)]
|
|
797
|
+
: [CONSOLE_MESSAGE_FIELDS, CONSOLE_MESSAGE_FIELDS_LEGACY];
|
|
798
|
+
let lastError: unknown = null;
|
|
799
|
+
|
|
800
|
+
for (const selectionSet of selectionSets) {
|
|
801
|
+
const updateMutation = `
|
|
802
|
+
mutation UpdateMessage($input: UpdateMessageInput!) {
|
|
803
|
+
updateMessage(input: $input) {
|
|
804
|
+
${selectionSet}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
`;
|
|
808
|
+
try {
|
|
809
|
+
const response = await getGraphQLClient().graphql({
|
|
810
|
+
query: updateMutation,
|
|
811
|
+
variables: { input: normalizedInput },
|
|
812
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
813
|
+
});
|
|
814
|
+
return normalizeConsoleMessageRecord(assertGraphQL<Record<string, unknown>>(response, "updateMessage"));
|
|
815
|
+
} catch (error) {
|
|
816
|
+
if (!isUnavailableQueryError(error)) throw normalizeUnknownError(error, "updateMessage");
|
|
817
|
+
lastError = error;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
throw normalizeUnknownError(lastError, "updateMessage");
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
export function subscribeConsoleMessages(
|
|
825
|
+
threadId: string,
|
|
826
|
+
onMessage: (message: ConsoleChatMessage) => void,
|
|
827
|
+
onError?: (error: unknown) => void,
|
|
828
|
+
): () => void {
|
|
829
|
+
configureAmplifyClient();
|
|
830
|
+
const client = generateClient<Schema>() as unknown as {
|
|
831
|
+
models: {
|
|
832
|
+
Message?: ConsoleMessageSubscriptionModel;
|
|
833
|
+
};
|
|
834
|
+
};
|
|
835
|
+
const messageModel = client.models.Message;
|
|
836
|
+
if (!messageModel || typeof messageModel.onCreate !== "function" || typeof messageModel.onUpdate !== "function") {
|
|
837
|
+
return () => undefined;
|
|
838
|
+
}
|
|
839
|
+
const handleEvent = (value: unknown) => {
|
|
840
|
+
const message = normalizeConsoleMessageSubscriptionPayload(value);
|
|
841
|
+
if (!message) return;
|
|
842
|
+
if (!message.threadId || message.threadId === threadId) onMessage(message);
|
|
843
|
+
};
|
|
844
|
+
const createSubscription = messageModel.onCreate().subscribe({ next: handleEvent, error: onError });
|
|
845
|
+
const updateSubscription = messageModel.onUpdate().subscribe({ next: handleEvent, error: onError });
|
|
846
|
+
return () => {
|
|
847
|
+
createSubscription.unsubscribe();
|
|
848
|
+
updateSubscription.unsubscribe();
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function normalizeConsoleMessageSubscriptionPayload(value: unknown): ConsoleChatMessage | null {
|
|
853
|
+
const record = resolveConsoleMessageSubscriptionRecord(value);
|
|
854
|
+
if (!record) return null;
|
|
855
|
+
return normalizeConsoleMessageRecord(record);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function resolveConsoleMessageSubscriptionRecord(value: unknown): Record<string, unknown> | null {
|
|
859
|
+
const queue: unknown[] = [value];
|
|
860
|
+
const visited = new Set<unknown>();
|
|
861
|
+
|
|
862
|
+
while (queue.length > 0) {
|
|
863
|
+
const current = queue.shift();
|
|
864
|
+
if (!current || typeof current !== "object" || visited.has(current)) continue;
|
|
865
|
+
visited.add(current);
|
|
866
|
+
const record = current as Record<string, unknown>;
|
|
867
|
+
if (typeof record.id === "string") return record;
|
|
868
|
+
|
|
869
|
+
for (const [key, nested] of Object.entries(record)) {
|
|
870
|
+
if (!nested || typeof nested !== "object") continue;
|
|
871
|
+
if (
|
|
872
|
+
key === "data"
|
|
873
|
+
|| key === "value"
|
|
874
|
+
|| key.startsWith("onCreate")
|
|
875
|
+
|| key.startsWith("onUpdate")
|
|
876
|
+
) {
|
|
877
|
+
queue.push(nested);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
async function listConsoleThreadsViaThreadModel(limit: number): Promise<ConsoleChatThread[]> {
|
|
886
|
+
const pageSize = Math.min(Math.max(limit, 100), 500);
|
|
887
|
+
const threads: ConsoleChatThread[] = [];
|
|
888
|
+
let nextToken: string | null = null;
|
|
889
|
+
|
|
890
|
+
do {
|
|
891
|
+
const queryResult: { page: ConsoleChatThread[]; nextCursor: string | null } = await runConsoleReadQuery({
|
|
892
|
+
field: "listMessageThreads",
|
|
893
|
+
query: LIST_CONSOLE_THREADS,
|
|
894
|
+
variables: {
|
|
895
|
+
limit: pageSize,
|
|
896
|
+
nextToken,
|
|
897
|
+
},
|
|
898
|
+
map: (response) => {
|
|
899
|
+
const connection = assertGraphQL<unknown>(response, "listMessageThreads");
|
|
900
|
+
return {
|
|
901
|
+
page: readConnectionItems<ConsoleChatThread>(connection),
|
|
902
|
+
nextCursor: readConnectionNextToken(connection),
|
|
903
|
+
};
|
|
904
|
+
},
|
|
905
|
+
});
|
|
906
|
+
threads.push(...queryResult.page);
|
|
907
|
+
nextToken = queryResult.nextCursor;
|
|
908
|
+
} while (nextToken && threads.length < limit);
|
|
909
|
+
|
|
910
|
+
return threads;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async function listConsoleMessagesRaw(pageSize: number, maxMessages: number): Promise<ConsoleChatMessage[]> {
|
|
914
|
+
const schema = await loadConsoleSchemaSnapshot();
|
|
915
|
+
const selectionSets = schema.messageFields.size
|
|
916
|
+
? [chooseMessageSelectionSet(schema.messageFields)]
|
|
917
|
+
: [CONSOLE_MESSAGE_FIELDS, CONSOLE_MESSAGE_FIELDS_LEGACY];
|
|
918
|
+
const attempts: Array<{
|
|
919
|
+
field: string;
|
|
920
|
+
query: string;
|
|
921
|
+
variables: Record<string, unknown>;
|
|
922
|
+
messageKinds?: string[];
|
|
923
|
+
}> = [];
|
|
924
|
+
|
|
925
|
+
const addDynamicAttempt = (
|
|
926
|
+
field: "listMessagesByNewsroomFeedAndCreatedAt" | "listMessagesByKindAndCreatedAt" | "listMessagesByDomainAndCreatedAt" | "listMessages",
|
|
927
|
+
selectionSet: string,
|
|
928
|
+
variables: Record<string, unknown>,
|
|
929
|
+
) => {
|
|
930
|
+
if (schema.queryFields.size > 0 && !schema.queryFields.has(field)) return;
|
|
931
|
+
attempts.push({
|
|
932
|
+
field,
|
|
933
|
+
query: buildListMessagesQuery(field, selectionSet),
|
|
934
|
+
variables,
|
|
935
|
+
});
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
for (const selectionSet of selectionSets) {
|
|
939
|
+
addDynamicAttempt("listMessagesByNewsroomFeedAndCreatedAt", selectionSet, {
|
|
940
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
941
|
+
limit: pageSize,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
if (schema.queryFields.size === 0 || schema.queryFields.has("listMessagesByKindAndCreatedAt")) {
|
|
945
|
+
attempts.push({
|
|
946
|
+
field: "listMessagesByKindAndCreatedAt",
|
|
947
|
+
query: buildListMessagesQuery("listMessagesByKindAndCreatedAt", selectionSet),
|
|
948
|
+
variables: { limit: pageSize },
|
|
949
|
+
messageKinds: [CONSOLE_MESSAGE_KIND, CONSOLE_TOOL_CALL_KIND, CONSOLE_TOOL_RESULT_KIND],
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
addDynamicAttempt("listMessagesByDomainAndCreatedAt", selectionSet, {
|
|
954
|
+
messageDomain: CONSOLE_MESSAGE_DOMAIN,
|
|
955
|
+
limit: pageSize,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
addDynamicAttempt("listMessages", selectionSet, {
|
|
959
|
+
limit: pageSize,
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (!attempts.length) {
|
|
964
|
+
attempts.push(
|
|
965
|
+
{
|
|
966
|
+
field: "listMessagesByNewsroomFeedAndCreatedAt",
|
|
967
|
+
query: LIST_CONSOLE_MESSAGES_BY_FEED_LEGACY,
|
|
968
|
+
variables: { newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY, limit: pageSize },
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
field: "listMessages",
|
|
972
|
+
query: LIST_CONSOLE_MESSAGES_LEGACY,
|
|
973
|
+
variables: { limit: pageSize },
|
|
974
|
+
},
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
let lastError: unknown = null;
|
|
979
|
+
for (const attempt of attempts) {
|
|
980
|
+
try {
|
|
981
|
+
if (attempt.messageKinds?.length && attempt.field === "listMessagesByKindAndCreatedAt") {
|
|
982
|
+
return await listMessagesByKindsWithQuery(
|
|
983
|
+
attempt.field,
|
|
984
|
+
attempt.query,
|
|
985
|
+
attempt.variables,
|
|
986
|
+
attempt.messageKinds,
|
|
987
|
+
maxMessages,
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
return await listMessagesWithQuery(attempt.field, attempt.query, attempt.variables, maxMessages);
|
|
991
|
+
} catch (error) {
|
|
992
|
+
if (!isUnavailableQueryError(error)) throw error;
|
|
993
|
+
lastError = error;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
throw lastError instanceof Error ? lastError : new Error("Console chat listMessages failed: no compatible query shape.");
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function listMessagesByKindsWithQuery(
|
|
1001
|
+
field: string,
|
|
1002
|
+
query: string,
|
|
1003
|
+
baseVariables: Record<string, unknown>,
|
|
1004
|
+
messageKinds: string[],
|
|
1005
|
+
maxMessages: number,
|
|
1006
|
+
): Promise<ConsoleChatMessage[]> {
|
|
1007
|
+
const byId = new Map<string, ConsoleChatMessage>();
|
|
1008
|
+
for (const messageKind of messageKinds) {
|
|
1009
|
+
const page = await listMessagesWithQuery(field, query, { ...baseVariables, messageKind }, maxMessages);
|
|
1010
|
+
for (const message of page) {
|
|
1011
|
+
byId.set(message.id, message);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return [...byId.values()].sort(
|
|
1015
|
+
(left, right) =>
|
|
1016
|
+
(left.sequenceNumber ?? 0) - (right.sequenceNumber ?? 0) ||
|
|
1017
|
+
left.createdAt.localeCompare(right.createdAt),
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
async function listMessagesWithQuery(
|
|
1022
|
+
field: string,
|
|
1023
|
+
query: string,
|
|
1024
|
+
baseVariables: Record<string, unknown>,
|
|
1025
|
+
maxMessages: number,
|
|
1026
|
+
): Promise<ConsoleChatMessage[]> {
|
|
1027
|
+
const messages: ConsoleChatMessage[] = [];
|
|
1028
|
+
let nextToken: string | null = null;
|
|
1029
|
+
|
|
1030
|
+
do {
|
|
1031
|
+
const queryResult: { page: ConsoleChatMessage[]; nextCursor: string | null } = await runConsoleReadQuery({
|
|
1032
|
+
field,
|
|
1033
|
+
query,
|
|
1034
|
+
variables: {
|
|
1035
|
+
...baseVariables,
|
|
1036
|
+
nextToken,
|
|
1037
|
+
},
|
|
1038
|
+
map: (response) => {
|
|
1039
|
+
const connection = assertGraphQL<unknown>(response, field, { allowPartialData: true });
|
|
1040
|
+
const items = readConnectionItems<Record<string, unknown> | null>(connection)
|
|
1041
|
+
.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === "object"))
|
|
1042
|
+
.map(normalizeConsoleMessageRecord);
|
|
1043
|
+
return {
|
|
1044
|
+
page: items,
|
|
1045
|
+
nextCursor: readConnectionNextToken(connection),
|
|
1046
|
+
};
|
|
1047
|
+
},
|
|
1048
|
+
});
|
|
1049
|
+
messages.push(...queryResult.page);
|
|
1050
|
+
nextToken = queryResult.nextCursor;
|
|
1051
|
+
} while (nextToken && messages.length < maxMessages);
|
|
1052
|
+
|
|
1053
|
+
return hydrateConsoleMessageBodies(messages);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async function hydrateConsoleMessageBodies(messages: ConsoleChatMessage[]): Promise<ConsoleChatMessage[]> {
|
|
1057
|
+
return Promise.all(messages.map(async (message) => {
|
|
1058
|
+
try {
|
|
1059
|
+
const payloads = await loadModelPayloadsForOwner("message", message.id, ["message_body"]);
|
|
1060
|
+
const body = payloads.find((payload) => payload.attachment.role === "message_body")?.text?.trim();
|
|
1061
|
+
if (body) return { ...message, content: body };
|
|
1062
|
+
} catch {
|
|
1063
|
+
// Fall back to inline field.
|
|
1064
|
+
}
|
|
1065
|
+
return message;
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function deriveConsoleThreadsFromMessages(messages: ConsoleChatMessage[], primaryAnchorKey: string): ConsoleChatThread[] {
|
|
1070
|
+
const threadlessMessages = messages.filter((message) => message.messageKind === CONSOLE_MESSAGE_KIND || !message.messageKind);
|
|
1071
|
+
const hasThreadIds = threadlessMessages.some((message) => message.threadId?.trim());
|
|
1072
|
+
if (!hasThreadIds) {
|
|
1073
|
+
if (!threadlessMessages.length) return [];
|
|
1074
|
+
const ordered = [...threadlessMessages].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
1075
|
+
const first = ordered[0];
|
|
1076
|
+
const last = ordered[ordered.length - 1];
|
|
1077
|
+
return [{
|
|
1078
|
+
id: "message-thread-console-default",
|
|
1079
|
+
threadKind: "console",
|
|
1080
|
+
status: "active",
|
|
1081
|
+
title: "Papyrus Console",
|
|
1082
|
+
summary: last.summary ?? last.content ?? null,
|
|
1083
|
+
primaryAnchorKind: "site",
|
|
1084
|
+
primaryAnchorId: "papyrus",
|
|
1085
|
+
primaryAnchorLineageId: "papyrus",
|
|
1086
|
+
primaryAnchorKey: primaryAnchorKey,
|
|
1087
|
+
createdByLabel: null,
|
|
1088
|
+
messageCount: ordered.length,
|
|
1089
|
+
lastMessageId: last.id,
|
|
1090
|
+
lastMessageAt: last.createdAt,
|
|
1091
|
+
contextDigest: null,
|
|
1092
|
+
metadata: null,
|
|
1093
|
+
createdAt: first.createdAt,
|
|
1094
|
+
updatedAt: last.createdAt,
|
|
1095
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
1096
|
+
}];
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const byThread = new Map<string, {
|
|
1100
|
+
count: number;
|
|
1101
|
+
firstCreatedAt: string;
|
|
1102
|
+
lastCreatedAt: string;
|
|
1103
|
+
lastMessageId: string;
|
|
1104
|
+
summary: string | null;
|
|
1105
|
+
}>();
|
|
1106
|
+
|
|
1107
|
+
for (const message of messages) {
|
|
1108
|
+
const threadId = message.threadId?.trim();
|
|
1109
|
+
if (!threadId) continue;
|
|
1110
|
+
if (message.messageKind && message.messageKind !== CONSOLE_MESSAGE_KIND) continue;
|
|
1111
|
+
const createdAt = message.createdAt ?? message.updatedAt ?? new Date().toISOString();
|
|
1112
|
+
const existing = byThread.get(threadId);
|
|
1113
|
+
if (!existing) {
|
|
1114
|
+
byThread.set(threadId, {
|
|
1115
|
+
count: 1,
|
|
1116
|
+
firstCreatedAt: createdAt,
|
|
1117
|
+
lastCreatedAt: createdAt,
|
|
1118
|
+
lastMessageId: message.id,
|
|
1119
|
+
summary: (message.summary ?? message.content ?? null),
|
|
1120
|
+
});
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
existing.count += 1;
|
|
1124
|
+
if (createdAt < existing.firstCreatedAt) existing.firstCreatedAt = createdAt;
|
|
1125
|
+
if (createdAt >= existing.lastCreatedAt) {
|
|
1126
|
+
existing.lastCreatedAt = createdAt;
|
|
1127
|
+
existing.lastMessageId = message.id;
|
|
1128
|
+
existing.summary = (message.summary ?? message.content ?? existing.summary);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return [...byThread.entries()]
|
|
1133
|
+
.map(([threadId, value]) => ({
|
|
1134
|
+
id: threadId,
|
|
1135
|
+
threadKind: "console",
|
|
1136
|
+
status: "active",
|
|
1137
|
+
title: "Papyrus Console",
|
|
1138
|
+
summary: value.summary,
|
|
1139
|
+
primaryAnchorKind: "site",
|
|
1140
|
+
primaryAnchorId: "papyrus",
|
|
1141
|
+
primaryAnchorLineageId: "papyrus",
|
|
1142
|
+
primaryAnchorKey: primaryAnchorKey,
|
|
1143
|
+
createdByLabel: null,
|
|
1144
|
+
messageCount: value.count,
|
|
1145
|
+
lastMessageId: value.lastMessageId,
|
|
1146
|
+
lastMessageAt: value.lastCreatedAt,
|
|
1147
|
+
contextDigest: null,
|
|
1148
|
+
metadata: null,
|
|
1149
|
+
createdAt: value.firstCreatedAt,
|
|
1150
|
+
updatedAt: value.lastCreatedAt,
|
|
1151
|
+
newsroomFeedKey: CONSOLE_NEWSROOM_FEED_KEY,
|
|
1152
|
+
}))
|
|
1153
|
+
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function isUnavailableQueryError(error: unknown): boolean {
|
|
1157
|
+
const text = (stringifyUnknownError(error) ?? "").toLowerCase();
|
|
1158
|
+
if (!text) return false;
|
|
1159
|
+
return (
|
|
1160
|
+
text.includes("unauthorized") ||
|
|
1161
|
+
text.includes("not authorized") ||
|
|
1162
|
+
text.includes("access denied") ||
|
|
1163
|
+
text.includes("fieldundefined") ||
|
|
1164
|
+
text.includes("cannot query field") ||
|
|
1165
|
+
text.includes("unknown type") ||
|
|
1166
|
+
text.includes("unknowntype") ||
|
|
1167
|
+
text.includes("validation error of type") ||
|
|
1168
|
+
text.includes("is not defined for input object type")
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
function normalizeConsoleMessageRecord(record: Record<string, unknown>): ConsoleChatMessage {
|
|
1173
|
+
const metadata = readJsonObject(record.metadata);
|
|
1174
|
+
const metadataConsole = readJsonObject(metadata?.console);
|
|
1175
|
+
const metadataConsoleAuthor = readJsonObject(metadataConsole?.author);
|
|
1176
|
+
const createdAt = readString(record.createdAt) ?? new Date().toISOString();
|
|
1177
|
+
const source = readString(record.source);
|
|
1178
|
+
const authorLabel = readString(record.authorLabel);
|
|
1179
|
+
const content = readString(record.content)
|
|
1180
|
+
?? readString(record.body)
|
|
1181
|
+
?? readString(metadata?.content)
|
|
1182
|
+
?? null;
|
|
1183
|
+
const inferredRole = inferConsoleMessageRole(record, metadata);
|
|
1184
|
+
const threadId = readString(record.threadId)
|
|
1185
|
+
?? readString(metadata?.threadId)
|
|
1186
|
+
?? readString(metadata?.messageThreadId)
|
|
1187
|
+
?? null;
|
|
1188
|
+
|
|
1189
|
+
return {
|
|
1190
|
+
id: readString(record.id) ?? `message-unknown-${createdAt}`,
|
|
1191
|
+
threadId,
|
|
1192
|
+
parentMessageId: readString(record.parentMessageId) ?? readString(metadata?.parentMessageId) ?? null,
|
|
1193
|
+
sequenceNumber: readNumber(record.sequenceNumber) ?? readNumber(metadata?.sequenceNumber) ?? null,
|
|
1194
|
+
role: readString(record.role) ?? readString(metadata?.role) ?? inferredRole,
|
|
1195
|
+
messageKind: readString(record.messageKind) ?? null,
|
|
1196
|
+
messageType: readString(record.messageType) ?? readString(metadata?.messageType) ?? null,
|
|
1197
|
+
content,
|
|
1198
|
+
summary: readString(record.summary) ?? null,
|
|
1199
|
+
responseStatus: readString(record.responseStatus) ?? readString(metadata?.responseStatus) ?? null,
|
|
1200
|
+
responseOwner: readString(record.responseOwner) ?? readString(metadata?.responseOwner) ?? null,
|
|
1201
|
+
responseStartedAt: readString(record.responseStartedAt) ?? readString(metadata?.responseStartedAt) ?? null,
|
|
1202
|
+
responseCompletedAt: readString(record.responseCompletedAt) ?? readString(metadata?.responseCompletedAt) ?? null,
|
|
1203
|
+
responseError: readString(record.responseError) ?? readString(metadata?.responseError) ?? null,
|
|
1204
|
+
source,
|
|
1205
|
+
authorUserProfileId: (
|
|
1206
|
+
readString(record.authorUserProfileId)
|
|
1207
|
+
?? readString(metadata?.authorUserProfileId)
|
|
1208
|
+
?? readString(metadataConsoleAuthor?.userProfileId)
|
|
1209
|
+
?? null
|
|
1210
|
+
),
|
|
1211
|
+
authorLabel,
|
|
1212
|
+
metadata,
|
|
1213
|
+
createdAt,
|
|
1214
|
+
updatedAt: readString(record.updatedAt) ?? null,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function inferConsoleMessageRole(
|
|
1219
|
+
record: Record<string, unknown>,
|
|
1220
|
+
metadata: Record<string, unknown> | null,
|
|
1221
|
+
): string | null {
|
|
1222
|
+
const id = readString(record.id)?.toLowerCase() ?? "";
|
|
1223
|
+
if (id.startsWith("message-console-user-")) return "USER";
|
|
1224
|
+
if (id.startsWith("message-console-assistant-")) return "ASSISTANT";
|
|
1225
|
+
if (id.startsWith("message-console-tool-")) return "TOOL";
|
|
1226
|
+
|
|
1227
|
+
const responseStatus = readString(record.responseStatus) ?? readString(metadata?.responseStatus);
|
|
1228
|
+
if (responseStatus === "PENDING") return "USER";
|
|
1229
|
+
if (responseStatus === "RUNNING" || responseStatus === "COMPLETED") return "ASSISTANT";
|
|
1230
|
+
|
|
1231
|
+
const messageKind = readString(record.messageKind);
|
|
1232
|
+
if (messageKind === CONSOLE_TOOL_CALL_KIND || messageKind === CONSOLE_TOOL_RESULT_KIND) return "TOOL";
|
|
1233
|
+
|
|
1234
|
+
const authorLabel = readString(record.authorLabel);
|
|
1235
|
+
const source = readString(record.source);
|
|
1236
|
+
if (authorLabel && source === "papyrus-console") return "USER";
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function readJsonObject(value: unknown): Record<string, unknown> | null {
|
|
1241
|
+
if (!value) return null;
|
|
1242
|
+
if (typeof value === "object" && !Array.isArray(value)) return value as Record<string, unknown>;
|
|
1243
|
+
if (typeof value === "string") {
|
|
1244
|
+
try {
|
|
1245
|
+
const parsed = JSON.parse(value);
|
|
1246
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed as Record<string, unknown>;
|
|
1247
|
+
} catch {
|
|
1248
|
+
return null;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function readString(value: unknown): string | null {
|
|
1255
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function readNumber(value: unknown): number | null {
|
|
1259
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
async function runConsoleReadQuery<T>({
|
|
1263
|
+
query,
|
|
1264
|
+
variables,
|
|
1265
|
+
field,
|
|
1266
|
+
map,
|
|
1267
|
+
}: {
|
|
1268
|
+
query: string;
|
|
1269
|
+
variables?: Record<string, unknown>;
|
|
1270
|
+
field: string;
|
|
1271
|
+
map: (response: {
|
|
1272
|
+
data?: Record<string, unknown> | null;
|
|
1273
|
+
errors?: Array<{ message?: string | null; errorType?: string | null } | string | null> | null;
|
|
1274
|
+
}) => T;
|
|
1275
|
+
}): Promise<T> {
|
|
1276
|
+
const client = getGraphQLClient();
|
|
1277
|
+
try {
|
|
1278
|
+
const response = await client.graphql({
|
|
1279
|
+
query,
|
|
1280
|
+
variables,
|
|
1281
|
+
authMode: USER_POOL_AUTH_MODE,
|
|
1282
|
+
});
|
|
1283
|
+
return map(response);
|
|
1284
|
+
} catch (firstError) {
|
|
1285
|
+
try {
|
|
1286
|
+
const response = await client.graphql({
|
|
1287
|
+
query,
|
|
1288
|
+
variables,
|
|
1289
|
+
authMode: API_KEY_AUTH_MODE,
|
|
1290
|
+
});
|
|
1291
|
+
return map(response);
|
|
1292
|
+
} catch (secondError) {
|
|
1293
|
+
throw normalizeUnknownError(secondError, field, firstError);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function normalizeUnknownError(error: unknown, operation: string, fallback?: unknown): Error {
|
|
1299
|
+
if (error instanceof Error && error.message.trim()) return error;
|
|
1300
|
+
const primary = stringifyUnknownError(error);
|
|
1301
|
+
const secondary = stringifyUnknownError(fallback);
|
|
1302
|
+
const details = [primary, secondary].filter(Boolean).join(" | ");
|
|
1303
|
+
return new Error(details ? `Console chat ${operation} failed: ${details}` : `Console chat ${operation} failed.`);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function stringifyUnknownError(error: unknown): string | null {
|
|
1307
|
+
if (!error) return null;
|
|
1308
|
+
if (typeof error === "string") return error;
|
|
1309
|
+
if (error instanceof Error) return error.message || error.name;
|
|
1310
|
+
try {
|
|
1311
|
+
const text = JSON.stringify(error);
|
|
1312
|
+
return text && text !== "{}" ? text : Object.prototype.toString.call(error);
|
|
1313
|
+
} catch {
|
|
1314
|
+
return Object.prototype.toString.call(error);
|
|
1315
|
+
}
|
|
1316
|
+
}
|