@segmentstream/agent 0.8.1 → 0.8.3
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/README.md +52 -0
- package/dist/_chunks/build-metadata-BLyaHLJW.mjs +16 -0
- package/dist/_chunks/{inbound-request-BwBvXk0A.mjs → inbound-request-CYSoY6pu.mjs} +35 -5
- package/dist/extension/config.d.ts +3 -0
- package/dist/extension/config.mjs +17 -2
- package/dist/extension/lib/attachment-tools.d.ts +5 -4
- package/dist/extension/lib/attachment-tools.mjs +3 -3
- package/dist/extension/lib/build-metadata.d.ts +8 -0
- package/dist/extension/lib/build-metadata.mjs +8 -0
- package/dist/extension/lib/confirmations.mjs +1 -1
- package/dist/extension/lib/deferred-confirmations.mjs +2 -2
- package/dist/extension/lib/ontology-tools.d.ts +9 -2
- package/dist/extension/lib/ontology-tools.mjs +48 -7
- package/dist/extension/lib/questions.d.ts +2 -0
- package/dist/extension/lib/stored-files.d.ts +17 -0
- package/dist/extension/lib/stored-files.mjs +70 -0
- package/dist/extension/lib/telegram/adapter.mjs +58 -4
- package/dist/extension/lib/telegram/attachment-description.mjs +1 -1
- package/dist/extension/lib/telegram/attachments-send.mjs +3 -3
- package/dist/extension/lib/telegram/confirmation-expiry.mjs +2 -2
- package/dist/extension/lib/telegram/confirmation-text.mjs +1 -1
- package/dist/extension/lib/telegram/confirmations.mjs +2 -2
- package/dist/extension/lib/telegram/content.d.ts +1 -1
- package/dist/extension/lib/telegram/conversations.mjs +1 -1
- package/dist/extension/lib/telegram/customer-text.d.ts +1 -1
- package/dist/extension/lib/telegram/customer-text.mjs +1 -1
- package/dist/extension/lib/telegram/deferred-confirmations.mjs +2 -2
- package/dist/extension/lib/telegram/files.mjs +1 -1
- package/dist/extension/lib/telegram/health.mjs +3 -1
- package/dist/extension/lib/telegram/images.mjs +1 -1
- package/dist/extension/lib/telegram/inbound-request.d.ts +11 -0
- package/dist/extension/lib/telegram/inbound-request.mjs +1 -1
- package/dist/extension/lib/telegram/inbound.d.ts +2 -0
- package/dist/extension/lib/telegram/inbound.mjs +147 -5
- package/dist/extension/lib/telegram/lifecycle.d.ts +46 -0
- package/dist/extension/lib/telegram/lifecycle.mjs +163 -0
- package/dist/extension/lib/telegram/media-policy.mjs +1 -1
- package/dist/extension/lib/telegram/silent.mjs +1 -1
- package/dist/extension/lib/telegram/turn-errors.mjs +1 -1
- package/dist/extension/lib/telegram/voice.mjs +1 -1
- package/dist/extension/tools/attachments_discard.d.ts +4 -4
- package/dist/extension/tools/attachments_list.d.ts +4 -4
- package/dist/extension/tools/attachments_promote.d.ts +4 -4
- package/dist/extension/tools/attachments_promote.mjs +12 -7
- package/dist/extension/tools/attachments_read.mjs +17 -4
- package/dist/extension/tools/attachments_table.d.ts +4 -4
- package/dist/extension/tools/questions_ask.d.ts +3 -1
- package/dist/extension/tools/questions_ask.mjs +2 -0
- package/package.json +3 -3
|
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
-
import { p as reportTelegramError } from "../../../_chunks/inbound-request-
|
|
7
|
+
import { p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
8
8
|
import { describableImage } from "./images.mjs";
|
|
9
9
|
import { MAXIMUM_ROUTING_PDF_CHARACTERS, extractRoutingPdf } from "./pdf-text.mjs";
|
|
10
10
|
import { gateway, generateText } from "ai";
|
|
@@ -6,11 +6,11 @@ __eveDirname(__filename);
|
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { appOrigin, channelsEnabled, proxyEnvironment } from "../environment.mjs";
|
|
8
8
|
import "./principal.mjs";
|
|
9
|
-
import { confirmationLocale } from "./confirmation-text.mjs";
|
|
10
9
|
import { redactDiagnostic } from "../diagnostics.mjs";
|
|
11
|
-
import {
|
|
10
|
+
import { confirmationLocale } from "./confirmation-text.mjs";
|
|
12
11
|
import { inTelegramSendQueue, telegramSendScope } from "./send-queue.mjs";
|
|
13
|
-
import { c as customerCopy, f as formattingRefused, l as customerMessage, p as reportTelegramError } from "../../../_chunks/inbound-request-
|
|
12
|
+
import { c as customerCopy, f as formattingRefused, l as customerMessage, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
13
|
+
import { runtimeToken } from "../runtime-token.mjs";
|
|
14
14
|
import { MAXIMUM_PROXY_BODY_BYTES, connectionProxy } from "../connection-proxy.mjs";
|
|
15
15
|
import { ACT_AS_HEADER } from "../ontology-connection.mjs";
|
|
16
16
|
import { AttachmentError, CONVERSATION_HEADER, SESSION_HEADER } from "../attachments.mjs";
|
|
@@ -5,11 +5,11 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { proxyEnvironment } from "../environment.mjs";
|
|
8
|
+
import { telegramSendScope } from "./send-queue.mjs";
|
|
9
|
+
import { m as safeTelegramFetch, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
8
10
|
import { runtimeToken } from "../runtime-token.mjs";
|
|
9
11
|
import { deferredConfirmations } from "../deferred-confirmations.mjs";
|
|
10
12
|
import { conversations } from "../conversations.mjs";
|
|
11
|
-
import { telegramSendScope } from "./send-queue.mjs";
|
|
12
|
-
import { m as safeTelegramFetch, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
|
|
13
13
|
import { expireDeferredRecord } from "./deferred-confirmations.mjs";
|
|
14
14
|
import { connectionProxy } from "../connection-proxy.mjs";
|
|
15
15
|
import { TELEGRAM_PLACEHOLDER_TOKEN, TELEGRAM_PROXY_ORIGIN, telegramProxyFetch } from "./proxy-fetch.mjs";
|
|
@@ -52,7 +52,7 @@ function confirmationCopy(value) {
|
|
|
52
52
|
function renderConfirmation(description, copy) {
|
|
53
53
|
copy = confirmationCopy(copy);
|
|
54
54
|
const action = humanText(description?.action);
|
|
55
|
-
if (description
|
|
55
|
+
if (description === void 0 || action === void 0 || action === "❓") throw new Error("A confirmation requires a safe declared action title.");
|
|
56
56
|
const facts = [[description?.warning ? "⚠️" : "", action].filter(Boolean).join(" "), ...(description?.fields ?? []).map(({ label, value }) => {
|
|
57
57
|
if (value.length > 3584) throw new Error("The complete confirmation facts cannot fit in one Telegram message.");
|
|
58
58
|
const safeLabel = humanText(label);
|
|
@@ -7,11 +7,11 @@ __eveCreateRequire(import.meta.url);
|
|
|
7
7
|
import { memberIdOf, telegramPrincipal } from "./principal.mjs";
|
|
8
8
|
import { humanText } from "../confirmation-description.mjs";
|
|
9
9
|
import { renderConfirmation, withConfirmationText } from "./confirmation-text.mjs";
|
|
10
|
-
import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
|
|
11
10
|
import { chatPostCard, chatPostCardStatus, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
|
|
11
|
+
import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
12
|
+
import { chatPostOperationId, isChatPost } from "../chat-post-confirmation.mjs";
|
|
12
13
|
import { confirmations } from "../confirmations.mjs";
|
|
13
14
|
import { conversationEvent } from "../conversations.mjs";
|
|
14
|
-
import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
|
|
15
15
|
import { formatAddress, scopedAddress } from "./sessions.mjs";
|
|
16
16
|
import { agentMessage, observedChatKey, withConfirmationPrompt } from "./observed.mjs";
|
|
17
17
|
import { postDeferredCard } from "./deferred-confirmations.mjs";
|
|
@@ -17,7 +17,7 @@ export declare function telegramRecord(value: unknown): Readonly<Record<string,
|
|
|
17
17
|
export declare function messageContent(raw: Readonly<Record<string, unknown>>): MessageContent;
|
|
18
18
|
/** Only shape/size diagnostics: do not retain/log raw webhook bodies, URLs or credential values. */
|
|
19
19
|
export declare function contentShape(raw: Readonly<Record<string, unknown>>, content?: MessageContent): {
|
|
20
|
-
contentSource: "
|
|
20
|
+
contentSource: "text" | "caption" | "none" | "rich_message";
|
|
21
21
|
textType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
22
22
|
captionType: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
23
23
|
hasRichMessage: boolean;
|
|
@@ -4,8 +4,8 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { n as currentInboundRequest, s as currentThreadId } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
7
8
|
import { conversationEvent } from "../conversations.mjs";
|
|
8
|
-
import { n as currentInboundRequest, s as currentThreadId } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
|
|
9
9
|
import { contentShape, messageContent } from "./content.mjs";
|
|
10
10
|
import { currentConfirmationPrompt, messageMetadata, observedChatKey } from "./observed.mjs";
|
|
11
11
|
import { archivedRoutingMessages } from "./routing-context.mjs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TelegramPreviewContext } from "@segmentstream-cloud/ontology/telegram";
|
|
2
2
|
import { type TelegramText } from "./format.js";
|
|
3
3
|
export { isTechnicalText } from "../customer-text-safety.js";
|
|
4
|
-
export declare function withDeliveryLocale<T>(locale: string, run: () => T, threadId?: string): T;
|
|
4
|
+
export declare function withDeliveryLocale<T>(locale: string, run: () => T, threadId?: string, replyTo?: string, chatId?: string): T;
|
|
5
5
|
export declare function currentLocale(): string;
|
|
6
6
|
export declare function currentThreadId(): string | undefined;
|
|
7
7
|
export declare function customerCopy(): {
|
|
@@ -5,5 +5,5 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { isTechnicalText } from "../customer-text-safety.mjs";
|
|
8
|
-
import { a as TelegramDeliveryError, c as customerCopy, d as failureText, f as formattingRefused, h as withDeliveryLocale, l as customerMessage, m as safeTelegramFetch, o as currentLocale, p as reportTelegramError, s as currentThreadId, u as customerText } from "../../../_chunks/inbound-request-
|
|
8
|
+
import { a as TelegramDeliveryError, c as customerCopy, d as failureText, f as formattingRefused, h as withDeliveryLocale, l as customerMessage, m as safeTelegramFetch, o as currentLocale, p as reportTelegramError, s as currentThreadId, u as customerText } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
9
9
|
export { TelegramDeliveryError, currentLocale, currentThreadId, customerCopy, customerMessage, customerText, failureText, formattingRefused, isTechnicalText, reportTelegramError, safeTelegramFetch, withDeliveryLocale };
|
|
@@ -7,12 +7,12 @@ __eveCreateRequire(import.meta.url);
|
|
|
7
7
|
import { telegramPrincipal } from "./principal.mjs";
|
|
8
8
|
import { humanText } from "../confirmation-description.mjs";
|
|
9
9
|
import { renderConfirmation, withConfirmationText } from "./confirmation-text.mjs";
|
|
10
|
-
import { confirmationOutcome } from "../confirmation-outcome.mjs";
|
|
11
10
|
import { chatPostCard, chatPostCardStatus, chatPostReceipt, withChatPostCard } from "./chat-post-card.mjs";
|
|
11
|
+
import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
12
12
|
import { CONFIRMATION_TTL_MS, inputHash } from "../confirmations.mjs";
|
|
13
|
+
import { confirmationOutcome } from "../confirmation-outcome.mjs";
|
|
13
14
|
import { deferredConfirmations, deferredKey, runDeferredAction } from "../deferred-confirmations.mjs";
|
|
14
15
|
import { conversationEvent } from "../conversations.mjs";
|
|
15
|
-
import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-BwBvXk0A.mjs";
|
|
16
16
|
import { formatAddress } from "./sessions.mjs";
|
|
17
17
|
import { agentMessage, observedChatKey, withConfirmationPrompt } from "./observed.mjs";
|
|
18
18
|
import { telegramTextLinks } from "@segmentstream-cloud/ontology/telegram";
|
|
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { conversationLanguage } from "./principal.mjs";
|
|
8
|
-
import { d as failureText, p as reportTelegramError } from "../../../_chunks/inbound-request-
|
|
8
|
+
import { d as failureText, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
9
9
|
import { describableImage, describeReceivedImage } from "./images.mjs";
|
|
10
10
|
import { MAXIMUM_ATTACHMENT_BYTES, attachmentUrl } from "../attachments.mjs";
|
|
11
11
|
function telegramConversation(chatId) {
|
|
@@ -4,6 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { t as agentBuild } from "../../../_chunks/build-metadata-BLyaHLJW.mjs";
|
|
7
8
|
import { runtimeRequestFailure } from "./runtime-request.mjs";
|
|
8
9
|
import { GET } from "eve/channels";
|
|
9
10
|
const HEALTH_ROUTE = "/eve/v1/telegram/health";
|
|
@@ -14,7 +15,8 @@ function healthRoute(input) {
|
|
|
14
15
|
const registry = input.chats.health();
|
|
15
16
|
return Response.json({
|
|
16
17
|
status: registry.degraded ? "degraded" : "ok",
|
|
17
|
-
registry
|
|
18
|
+
registry,
|
|
19
|
+
runtime: agentBuild()
|
|
18
20
|
}, {
|
|
19
21
|
status: registry.degraded ? 503 : 200,
|
|
20
22
|
headers: { "cache-control": "no-store" }
|
|
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
-
import { p as reportTelegramError } from "../../../_chunks/inbound-request-
|
|
7
|
+
import { p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
8
8
|
import { gateway, generateText } from "ai";
|
|
9
9
|
const MAXIMUM_IMAGE_DESCRIPTION_BYTES = 5242880;
|
|
10
10
|
const MAXIMUM_NATIVE_IMAGE_BYTES = 3145728;
|
|
@@ -29,7 +29,16 @@ export declare class InboundRequest {
|
|
|
29
29
|
/** The agent's description from the compiled manifest, for the routing decision. */
|
|
30
30
|
readonly agentDescription: () => Promise<string>;
|
|
31
31
|
readonly agentName: () => Promise<string>;
|
|
32
|
+
readonly traceId: `${string}-${string}-${string}-${string}-${string}`;
|
|
32
33
|
edited: boolean;
|
|
34
|
+
triggeringMessageId: string | undefined;
|
|
35
|
+
chatId: string | undefined;
|
|
36
|
+
replyTo: string | undefined;
|
|
37
|
+
stopTyping: (() => void) | undefined;
|
|
38
|
+
lifecycle: ReturnType<typeof import("./lifecycle.js").attachmentLifecycle> | undefined;
|
|
39
|
+
lifecycleKey: string | undefined;
|
|
40
|
+
lifecycleContext: string | undefined;
|
|
41
|
+
monitor: ((session: Awaited<ReturnType<ChannelSource<unknown>["send"]>>, marker: string) => void) | undefined;
|
|
33
42
|
previousText: string | undefined;
|
|
34
43
|
attachmentsChanged: boolean;
|
|
35
44
|
/** Evidence may be reused only while the provider attachment identities are unchanged. */
|
|
@@ -58,6 +67,8 @@ export declare class InboundRequest {
|
|
|
58
67
|
extraFiles: Exclude<UserContent, string> | undefined;
|
|
59
68
|
/** The route's session operations, on addresses already mapped to the configured scope. */
|
|
60
69
|
sessions: SessionOperations | undefined;
|
|
70
|
+
/** Host background lifetime; long-running observers must never enter the routing drain. */
|
|
71
|
+
backgroundUntil: ((task: Promise<unknown>) => void) | undefined;
|
|
61
72
|
/** Keeps work the request started, such as a chat report, alive after it answers. */
|
|
62
73
|
waitUntil: ((task: Promise<unknown>) => void) | undefined;
|
|
63
74
|
/** Continues the exact pending question's session, including under thread scope. */
|
|
@@ -4,5 +4,5 @@ import { createRequire as __eveCreateRequire } from "node:module";
|
|
|
4
4
|
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
|
-
import { i as withReplacedText, n as currentInboundRequest, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-
|
|
7
|
+
import { i as withReplacedText, n as currentInboundRequest, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
8
8
|
export { InboundRequest, currentInboundRequest, runInboundRequest, withReplacedText };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { attachmentLifecycle } from "./lifecycle.js";
|
|
1
2
|
import type { TelegramContext, TelegramInboundResult, TelegramMessage } from "eve/channels/telegram";
|
|
2
3
|
import type { AdmissionSettings, SessionSettings, TelegramPolicy } from "../../config.js";
|
|
3
4
|
import type { AgentAttachments } from "../attachments.js";
|
|
@@ -38,6 +39,7 @@ import type { PdfTextExtractor } from "./pdf-text.js";
|
|
|
38
39
|
*/
|
|
39
40
|
export interface InboundDependencies {
|
|
40
41
|
readonly policy: TelegramPolicy;
|
|
42
|
+
readonly lifecycle?: (principal: import("eve/context").SessionAuthContext, memberId: string, message: TelegramMessage) => ReturnType<typeof attachmentLifecycle>;
|
|
41
43
|
readonly admission: () => AdmissionSettings;
|
|
42
44
|
readonly sessions: () => SessionSettings;
|
|
43
45
|
readonly sessionPolicy: SessionPolicy;
|
|
@@ -5,7 +5,8 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { conversationLanguage, telegramPrincipal } from "./principal.mjs";
|
|
8
|
-
import { p as reportTelegramError } from "../../../_chunks/inbound-request-
|
|
8
|
+
import { p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
9
|
+
import { startTypingHeartbeat, typingUntilComplete } from "./lifecycle.mjs";
|
|
9
10
|
import { formatAddress, messageAddress, scopedAddress } from "./sessions.mjs";
|
|
10
11
|
import { contentShape, messageContent } from "./content.mjs";
|
|
11
12
|
import { messageAudience, otherRecipient } from "./audience.mjs";
|
|
@@ -363,6 +364,65 @@ async function handleInboundMessage(input) {
|
|
|
363
364
|
turnAdmitted = true;
|
|
364
365
|
return true;
|
|
365
366
|
}
|
|
367
|
+
if (input.request !== void 0 && sent.attachments.length > 0 && dependencies.lifecycle !== void 0 && await admitTurn(true)) {
|
|
368
|
+
const principal = telegramPrincipal({
|
|
369
|
+
message: sent,
|
|
370
|
+
member
|
|
371
|
+
});
|
|
372
|
+
input.request.lifecycle = dependencies.lifecycle(principal, member.id, sent);
|
|
373
|
+
input.request.stopTyping = startTypingHeartbeat(context.telegram);
|
|
374
|
+
try {
|
|
375
|
+
const admitted = await input.request.lifecycle("admitted", { attachments: sent.attachments.map((file) => ({
|
|
376
|
+
id: file.fileId,
|
|
377
|
+
filename: file.fileName ?? file.kind,
|
|
378
|
+
mediaType: file.mediaType,
|
|
379
|
+
kind: file.kind
|
|
380
|
+
})) });
|
|
381
|
+
input.request.lifecycleKey = admitted.key;
|
|
382
|
+
input.request.replyTo = admitted.replyTo ?? sent.messageId;
|
|
383
|
+
if (admitted.key === void 0) input.request.lifecycle = void 0;
|
|
384
|
+
if (admitted.acknowledgement) try {
|
|
385
|
+
await context.telegram.post({
|
|
386
|
+
text: admitted.acknowledgement,
|
|
387
|
+
reply_parameters: { message_id: Number(input.request.replyTo) }
|
|
388
|
+
});
|
|
389
|
+
console.info("[segmentstream] telegram.phase", {
|
|
390
|
+
phase: "acknowledged",
|
|
391
|
+
at: new Date().toISOString(),
|
|
392
|
+
traceId: input.request.traceId,
|
|
393
|
+
intakeId: admitted.key,
|
|
394
|
+
chatId: sent.chat.id,
|
|
395
|
+
messageId: sent.messageId
|
|
396
|
+
});
|
|
397
|
+
} catch (error) {
|
|
398
|
+
reportTelegramError("intake_acknowledgement_failed", error, {
|
|
399
|
+
chatId: sent.chat.id,
|
|
400
|
+
messageId: sent.messageId
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
reportTelegramError("intake_admission_failed", error, {
|
|
405
|
+
chatId: sent.chat.id,
|
|
406
|
+
messageId: sent.messageId
|
|
407
|
+
});
|
|
408
|
+
input.request.lifecycle = void 0;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (input.request !== void 0) {
|
|
412
|
+
input.request.triggeringMessageId = sent.messageId;
|
|
413
|
+
input.request.chatId = sent.chat.id;
|
|
414
|
+
input.request.replyTo ??= sent.messageId;
|
|
415
|
+
input.request.monitor = (session, marker) => {
|
|
416
|
+
input.request?.backgroundUntil?.(typingUntilComplete(session, marker, context.telegram, AbortSignal.timeout(6e5), {
|
|
417
|
+
traceId: input.request?.traceId,
|
|
418
|
+
intakeId: input.request?.lifecycleKey,
|
|
419
|
+
chatId: sent.chat.id,
|
|
420
|
+
messageId: sent.messageId
|
|
421
|
+
}).catch((error) => {
|
|
422
|
+
reportTelegramError("typing_monitor_failed", error, { sessionId: session.id });
|
|
423
|
+
}));
|
|
424
|
+
};
|
|
425
|
+
}
|
|
366
426
|
let address = scopedAddress(messageAddress(sent), dependencies.sessions().scope);
|
|
367
427
|
const operations = input.request?.sessions;
|
|
368
428
|
const topicAddress = formatAddress({
|
|
@@ -380,7 +440,7 @@ async function handleInboundMessage(input) {
|
|
|
380
440
|
const owner = questionSession ?? await operations?.resolveSession(address) ?? await operations?.resolveSession(topicAddress);
|
|
381
441
|
const questionHistory = owner === void 0 || dependencies.history === void 0 ? [] : await dependencies.history(owner.id);
|
|
382
442
|
const pending = questionFromHistory(questionHistory, questionOwner);
|
|
383
|
-
let answering = pending !== void 0 && pending.memberId === member.id && pending.chatId === sent.chat.id && pending.messageThreadId === String(sent.messageThreadId ?? "");
|
|
443
|
+
let answering = pending !== void 0 && (input.request?.lifecycleKey === void 0 || pending.taskId === input.request.lifecycleKey) && pending.memberId === member.id && pending.chatId === sent.chat.id && pending.messageThreadId === String(sent.messageThreadId ?? "");
|
|
384
444
|
const asked = questionHistory.findLast((event) => {
|
|
385
445
|
const data = isRecord(event.data) ? event.data : {};
|
|
386
446
|
const result = isRecord(data.result) ? data.result : {};
|
|
@@ -393,7 +453,7 @@ async function handleInboundMessage(input) {
|
|
|
393
453
|
address = pending.address || (questionSession === void 0 ? topicAddress : memberQuestionAddress);
|
|
394
454
|
input.request.address = address;
|
|
395
455
|
}
|
|
396
|
-
if (group && (addressed === void 0 && !answering && dependencies.policy === "mention" || elsewhere !== void 0 && (addressed === void 0 || addressed === "reply-to-bot" && !recipientAnswer))) {
|
|
456
|
+
if (input.request?.lifecycleKey === void 0 && group && (addressed === void 0 && !answering && dependencies.policy === "mention" || elsewhere !== void 0 && (addressed === void 0 || addressed === "reply-to-bot" && !recipientAnswer))) {
|
|
397
457
|
await recordInboundContent({
|
|
398
458
|
text,
|
|
399
459
|
decoded
|
|
@@ -481,7 +541,7 @@ async function handleInboundMessage(input) {
|
|
|
481
541
|
};
|
|
482
542
|
return question;
|
|
483
543
|
};
|
|
484
|
-
const route = await routeMessage({
|
|
544
|
+
const route = input.request?.lifecycleKey !== void 0 ? "reply" : await routeMessage({
|
|
485
545
|
chatId: sent.chat.id,
|
|
486
546
|
memberId: member.id,
|
|
487
547
|
policy: dependencies.policy,
|
|
@@ -539,6 +599,10 @@ async function handleInboundMessage(input) {
|
|
|
539
599
|
return conversation();
|
|
540
600
|
}
|
|
541
601
|
});
|
|
602
|
+
if (input.request?.lifecycleKey !== void 0) recordRouteDecision("reply", addressed, dependencies.policy, {
|
|
603
|
+
reason: "workspace_attachment_lifecycle",
|
|
604
|
+
note: "The configured workspace action admitted this attachment task."
|
|
605
|
+
});
|
|
542
606
|
if (admissionRefused) {
|
|
543
607
|
await recordInboundContent({
|
|
544
608
|
text,
|
|
@@ -564,6 +628,66 @@ async function handleInboundMessage(input) {
|
|
|
564
628
|
} : mediaPlan },
|
|
565
629
|
...dependencies.describeImage === void 0 ? {} : { describeImage: dependencies.describeImage }
|
|
566
630
|
});
|
|
631
|
+
if (input.request?.lifecycle !== void 0) {
|
|
632
|
+
const lifecycle = input.request.lifecycle;
|
|
633
|
+
const key = input.request.lifecycleKey;
|
|
634
|
+
for (const [sourceFile, file] of files.entries()) {
|
|
635
|
+
if (file.status === "refused") {
|
|
636
|
+
await lifecycle("stored", {
|
|
637
|
+
key,
|
|
638
|
+
sourceFile,
|
|
639
|
+
error: {
|
|
640
|
+
filename: file.filename,
|
|
641
|
+
reason: file.reason
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const stored = await lifecycle("stored", {
|
|
647
|
+
key,
|
|
648
|
+
sourceFile,
|
|
649
|
+
attachment: {
|
|
650
|
+
id: file.attachment.id,
|
|
651
|
+
filename: file.attachment.filename,
|
|
652
|
+
sha256: file.attachment.sha256
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
if (stored.retain !== void 0 && dependencies.attachments !== void 0) {
|
|
656
|
+
let outcome;
|
|
657
|
+
try {
|
|
658
|
+
outcome = { result: (await dependencies.attachments.promote({
|
|
659
|
+
memberId: member.id,
|
|
660
|
+
conversation: `telegram:${sent.chat.id}`
|
|
661
|
+
}, file.attachment.id, stored.retain)).result };
|
|
662
|
+
} catch (error) {
|
|
663
|
+
outcome = { error: reportTelegramError("attachment_retention_failed", error, { attachmentId: file.attachment.id }) };
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
await lifecycle("retained", {
|
|
667
|
+
key,
|
|
668
|
+
attachment: file.attachment.id,
|
|
669
|
+
...outcome
|
|
670
|
+
});
|
|
671
|
+
} catch (error) {
|
|
672
|
+
reportTelegramError("attachment_receipt_recording_failed", error, { attachmentId: file.attachment.id });
|
|
673
|
+
await lifecycle("retained", {
|
|
674
|
+
key,
|
|
675
|
+
attachment: file.attachment.id,
|
|
676
|
+
...outcome
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
let current = await lifecycle("ready", { key });
|
|
682
|
+
const collectionTimeoutAt = Date.now() + 12e4;
|
|
683
|
+
while (current.readyAt !== void 0 && current.readyAt > Date.now()) {
|
|
684
|
+
if (Date.now() >= collectionTimeoutAt) throw new Error("The attachment collection exceeded its processing budget.");
|
|
685
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(2e4, current.readyAt - Date.now(), collectionTimeoutAt - Date.now())));
|
|
686
|
+
current = await lifecycle("ready", { key });
|
|
687
|
+
}
|
|
688
|
+
input.request.lifecycleContext = current.context;
|
|
689
|
+
input.request.replyTo = current.replyTo ?? input.request.replyTo;
|
|
690
|
+
}
|
|
567
691
|
if (routingFile !== void 0 && processedMedia !== void 0) {
|
|
568
692
|
const stored = files.get(routingFile.fileId);
|
|
569
693
|
if (stored?.status === "stored" && stored.imageInput === "text") files = new Map(files).set(routingFile.fileId, {
|
|
@@ -599,6 +723,8 @@ async function handleInboundMessage(input) {
|
|
|
599
723
|
if (voice !== void 0 && input.request !== void 0) input.request.text = text;
|
|
600
724
|
else if (voice !== void 0) notes.push(text);
|
|
601
725
|
if (answering && pending !== void 0 && input.request !== void 0) {
|
|
726
|
+
input.request.replyTo = pending.replyToMessageId ?? input.request.replyTo;
|
|
727
|
+
input.request.lifecycleKey = pending.taskId ?? input.request.lifecycleKey;
|
|
602
728
|
input.request.text = `${answerMarker(pending.id)}\n${text}`;
|
|
603
729
|
notes.push(`<pending_question>\nTask: ${pending.task}\nQuestion: ${pending.question}\nUse this member's answer to continue the pending task with their current permissions.
|
|
604
730
|
</pending_question>`);
|
|
@@ -667,10 +793,26 @@ async function handleInboundMessage(input) {
|
|
|
667
793
|
...principal,
|
|
668
794
|
attributes: {
|
|
669
795
|
...principal.attributes,
|
|
796
|
+
replyToMessageId: input.request?.replyTo ?? sent.messageId,
|
|
797
|
+
...input.request?.lifecycleKey === void 0 ? {} : {
|
|
798
|
+
taskId: input.request.lifecycleKey,
|
|
799
|
+
triggeringMessageId: sent.messageId
|
|
800
|
+
},
|
|
670
801
|
pendingQuestionId: pending.id
|
|
671
802
|
}
|
|
672
|
-
} :
|
|
803
|
+
} : {
|
|
804
|
+
...principal,
|
|
805
|
+
attributes: {
|
|
806
|
+
...principal.attributes,
|
|
807
|
+
replyToMessageId: input.request?.replyTo ?? sent.messageId,
|
|
808
|
+
...input.request?.lifecycleKey === void 0 ? {} : {
|
|
809
|
+
taskId: input.request.lifecycleKey,
|
|
810
|
+
triggeringMessageId: sent.messageId
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
},
|
|
673
814
|
context: [
|
|
815
|
+
...input.request?.lifecycleContext === void 0 ? [] : [input.request.lifecycleContext],
|
|
674
816
|
...observedContext(dependencies.observed.take(chat), sharedContext),
|
|
675
817
|
...group ? chatContext(dependencies.chats.chat(sent.chat.id)) : [],
|
|
676
818
|
speakerBlock({
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SessionAuthContext } from "eve/context";
|
|
2
|
+
import type { TelegramContext } from "eve/channels/telegram";
|
|
3
|
+
import type { Session } from "eve/channels";
|
|
4
|
+
import type { Environment } from "../environment.js";
|
|
5
|
+
import type { RuntimeToken } from "../runtime-token.js";
|
|
6
|
+
export interface AttachmentLifecycle {
|
|
7
|
+
readonly entity: string;
|
|
8
|
+
readonly action: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LifecycleResult {
|
|
11
|
+
readonly key?: string;
|
|
12
|
+
readonly replyTo?: string;
|
|
13
|
+
readonly acknowledgement?: string;
|
|
14
|
+
readonly readyAt?: number;
|
|
15
|
+
readonly context?: string;
|
|
16
|
+
readonly complete?: boolean;
|
|
17
|
+
readonly message?: string;
|
|
18
|
+
readonly retain?: {
|
|
19
|
+
readonly entity: string;
|
|
20
|
+
readonly action: string;
|
|
21
|
+
readonly input: Record<string, unknown>;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** Opt-in workspace action; the workspace owns grouping, copy, retention and completion decisions. */
|
|
25
|
+
export declare function attachmentLifecycle(options: {
|
|
26
|
+
readonly configuration: AttachmentLifecycle;
|
|
27
|
+
readonly principal: SessionAuthContext;
|
|
28
|
+
readonly memberId: string;
|
|
29
|
+
readonly message: {
|
|
30
|
+
readonly chat: {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
};
|
|
33
|
+
readonly messageId: string;
|
|
34
|
+
readonly messageThreadId?: number;
|
|
35
|
+
readonly from?: {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
readonly environment?: Environment;
|
|
40
|
+
readonly token?: RuntimeToken;
|
|
41
|
+
readonly fetch?: typeof fetch;
|
|
42
|
+
}): (phase: "admitted" | "stored" | "retained" | "ready" | "completed", data?: Record<string, unknown>) => Promise<LifecycleResult>;
|
|
43
|
+
/** Kept inside the webhook's waitUntil lifetime; terminal durable stream events close the reader. */
|
|
44
|
+
export declare function typingUntilComplete(session: Session, marker: string, telegram: TelegramContext["telegram"], signal: AbortSignal, metadata?: Readonly<Record<string, string | undefined>>): Promise<void>;
|
|
45
|
+
/** No global timer survives a request or terminal event; failures retain safe method/status telemetry. */
|
|
46
|
+
export declare function startTypingHeartbeat(telegram: TelegramContext["telegram"]): () => void;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { fileURLToPath as __eveFileURLToPath } from "node:url";
|
|
2
|
+
import { dirname as __eveDirname } from "node:path";
|
|
3
|
+
import { createRequire as __eveCreateRequire } from "node:module";
|
|
4
|
+
const __filename = __eveFileURLToPath(import.meta.url);
|
|
5
|
+
__eveDirname(__filename);
|
|
6
|
+
__eveCreateRequire(import.meta.url);
|
|
7
|
+
import { t as agentBuild } from "../../../_chunks/build-metadata-BLyaHLJW.mjs";
|
|
8
|
+
import { object } from "../confirmation-description.mjs";
|
|
9
|
+
import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
10
|
+
import { needsConfirmation } from "../confirmations.mjs";
|
|
11
|
+
import { actionToolName, ontologyTools } from "../ontology-tools.mjs";
|
|
12
|
+
function attachmentLifecycle(options) {
|
|
13
|
+
const tools = ontologyTools(options);
|
|
14
|
+
const action = actionToolName(options.configuration.entity, options.configuration.action);
|
|
15
|
+
return async (phase, data = {}) => {
|
|
16
|
+
console.info("[segmentstream] telegram.phase", {
|
|
17
|
+
phase,
|
|
18
|
+
traceId: currentInboundRequest()?.traceId,
|
|
19
|
+
at: new Date().toISOString(),
|
|
20
|
+
chatId: options.message.chat.id,
|
|
21
|
+
messageId: options.message.messageId,
|
|
22
|
+
memberId: options.memberId,
|
|
23
|
+
intakeId: data.key,
|
|
24
|
+
runtime: agentBuild(options.environment)
|
|
25
|
+
});
|
|
26
|
+
const input = {
|
|
27
|
+
phase,
|
|
28
|
+
source: {
|
|
29
|
+
chat: options.message.chat.id,
|
|
30
|
+
topic: String(options.message.messageThreadId ?? ""),
|
|
31
|
+
message: options.message.messageId,
|
|
32
|
+
sender: options.message.from.id,
|
|
33
|
+
locale: String(options.principal.attributes.conversationLanguage ?? options.principal.attributes.locale ?? "en")
|
|
34
|
+
},
|
|
35
|
+
data
|
|
36
|
+
};
|
|
37
|
+
if (needsConfirmation(await tools.labels(options.memberId, action, input))) throw new Error("An attachment lifecycle hook must be a permitted routine action.");
|
|
38
|
+
const raw = await tools.rpc(options.memberId, "tools/call", {
|
|
39
|
+
name: action,
|
|
40
|
+
arguments: input
|
|
41
|
+
});
|
|
42
|
+
const envelope = object(raw.structuredContent);
|
|
43
|
+
if (object(envelope.validation).ok === false) throw new Error("The attachment lifecycle action rejected its event.");
|
|
44
|
+
const result = object(envelope.result ?? envelope);
|
|
45
|
+
const retention = object(result.retain);
|
|
46
|
+
if (typeof retention.entity === "string" && typeof retention.action === "string" && needsConfirmation(await tools.labels(options.memberId, actionToolName(retention.entity, retention.action), object(retention.input)))) throw new Error("Automatic retention requires a permitted routine storage action.");
|
|
47
|
+
return {
|
|
48
|
+
...typeof result.key === "string" ? { key: result.key } : {},
|
|
49
|
+
...typeof result.reply_to === "string" && /^[1-9]\d*$/.test(result.reply_to) ? { replyTo: result.reply_to } : {},
|
|
50
|
+
...typeof result.acknowledgement === "string" ? { acknowledgement: result.acknowledgement } : {},
|
|
51
|
+
...typeof result.ready_at === "number" && Number.isSafeInteger(result.ready_at) ? { readyAt: result.ready_at } : {},
|
|
52
|
+
...typeof result.context === "string" ? { context: result.context } : {},
|
|
53
|
+
...typeof result.complete === "boolean" ? { complete: result.complete } : {},
|
|
54
|
+
...typeof result.message === "string" ? { message: result.message } : {},
|
|
55
|
+
...typeof object(result.retain).entity === "string" && typeof object(result.retain).action === "string" ? { retain: {
|
|
56
|
+
entity: String(object(result.retain).entity),
|
|
57
|
+
action: String(object(result.retain).action),
|
|
58
|
+
input: object(object(result.retain).input)
|
|
59
|
+
} } : {}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
async function typingUntilComplete(session, marker, telegram, signal, metadata = {}) {
|
|
64
|
+
const reader = (await session.getEventStream({ follow: true })).getReader();
|
|
65
|
+
let turn;
|
|
66
|
+
let firstResponse = false;
|
|
67
|
+
const abort = () => {
|
|
68
|
+
reader.cancel().catch(() => void 0);
|
|
69
|
+
};
|
|
70
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
71
|
+
const stop = startTypingHeartbeat(telegram);
|
|
72
|
+
try {
|
|
73
|
+
while (!signal.aborted) {
|
|
74
|
+
const item = await reader.read();
|
|
75
|
+
if (item.done) break;
|
|
76
|
+
const event = item.value;
|
|
77
|
+
if (event.type === "message.received" && event.data.message.includes(marker)) {
|
|
78
|
+
turn = event.data.turnId;
|
|
79
|
+
console.info("[segmentstream] telegram.phase", {
|
|
80
|
+
...metadata,
|
|
81
|
+
phase: "turn_received",
|
|
82
|
+
at: new Date().toISOString(),
|
|
83
|
+
sessionId: session.id,
|
|
84
|
+
turnId: turn
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (turn && event.data !== void 0 && "turnId" in event.data && event.data.turnId === turn && [
|
|
88
|
+
"step.started",
|
|
89
|
+
"actions.requested",
|
|
90
|
+
"action.result",
|
|
91
|
+
"turn.completed",
|
|
92
|
+
"turn.failed",
|
|
93
|
+
"turn.cancelled"
|
|
94
|
+
].includes(event.type)) console.info("[segmentstream] telegram.phase", {
|
|
95
|
+
...metadata,
|
|
96
|
+
phase: event.type,
|
|
97
|
+
at: new Date().toISOString(),
|
|
98
|
+
sessionId: session.id,
|
|
99
|
+
turnId: turn
|
|
100
|
+
});
|
|
101
|
+
if (turn && !firstResponse && (event.type === "message.appended" || event.type === "reasoning.appended") && "turnId" in event.data && event.data.turnId === turn) {
|
|
102
|
+
firstResponse = true;
|
|
103
|
+
console.info("[segmentstream] telegram.phase", {
|
|
104
|
+
...metadata,
|
|
105
|
+
phase: "model_first_response",
|
|
106
|
+
at: new Date().toISOString(),
|
|
107
|
+
sessionId: session.id,
|
|
108
|
+
turnId: turn
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (event.type === "session.failed" || event.type === "session.completed") break;
|
|
112
|
+
if (turn && "turnId" in event.data && event.data.turnId === turn && ([
|
|
113
|
+
"turn.completed",
|
|
114
|
+
"turn.failed",
|
|
115
|
+
"turn.cancelled",
|
|
116
|
+
"input.requested"
|
|
117
|
+
].includes(event.type) || event.type === "turn.waiting" && event.data.on === "input")) break;
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
stop();
|
|
121
|
+
signal.removeEventListener("abort", abort);
|
|
122
|
+
await reader.cancel().catch(() => void 0);
|
|
123
|
+
reader.releaseLock();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function startTypingHeartbeat(telegram) {
|
|
127
|
+
let stopped = false;
|
|
128
|
+
let timer;
|
|
129
|
+
const tick = async () => {
|
|
130
|
+
try {
|
|
131
|
+
const response = await telegram.request("sendChatAction", {
|
|
132
|
+
chat_id: telegram.chatId,
|
|
133
|
+
action: "typing",
|
|
134
|
+
...telegram.messageThreadId === void 0 ? {} : { message_thread_id: telegram.messageThreadId }
|
|
135
|
+
});
|
|
136
|
+
console.info("[segmentstream] telegram.method", {
|
|
137
|
+
method: "sendChatAction",
|
|
138
|
+
at: new Date().toISOString(),
|
|
139
|
+
chatId: telegram.chatId,
|
|
140
|
+
status: response.status,
|
|
141
|
+
accepted: response.ok && object(response.body).ok === true
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok || object(response.body).ok !== true) throw new Error("Telegram rejected sendChatAction.", { cause: {
|
|
144
|
+
status: response.status,
|
|
145
|
+
body: response.body
|
|
146
|
+
} });
|
|
147
|
+
} catch (error) {
|
|
148
|
+
reportTelegramError("typing_failed", error, {
|
|
149
|
+
method: "sendChatAction",
|
|
150
|
+
chatId: telegram.chatId
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (!stopped) timer = setTimeout(() => {
|
|
154
|
+
tick();
|
|
155
|
+
}, 4e3);
|
|
156
|
+
};
|
|
157
|
+
tick();
|
|
158
|
+
return () => {
|
|
159
|
+
stopped = true;
|
|
160
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export { attachmentLifecycle, startTypingHeartbeat, typingUntilComplete };
|
|
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
|
|
|
5
5
|
__eveDirname(__filename);
|
|
6
6
|
__eveCreateRequire(import.meta.url);
|
|
7
7
|
import { DEFAULT_IMAGE_DESCRIPTION_MODEL } from "../../config.mjs";
|
|
8
|
-
import { p as reportTelegramError } from "../../../_chunks/inbound-request-
|
|
8
|
+
import { p as reportTelegramError } from "../../../_chunks/inbound-request-CYSoY6pu.mjs";
|
|
9
9
|
function planMedia(capabilities, descriptionModel = null) {
|
|
10
10
|
if (typeof descriptionModel === "string") return {
|
|
11
11
|
images: "describe",
|