@okrlinkhub/agent-factory 2.0.1 → 2.0.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.
@@ -1,12 +1,13 @@
1
1
  import { v } from "convex/values";
2
- import { internal } from "./_generated/api.js";
2
+ import { api, internal } from "./_generated/api.js";
3
3
  import {
4
+ action,
4
5
  internalMutation,
5
6
  internalQuery,
6
7
  mutation,
7
8
  query,
8
9
  } from "./_generated/server.js";
9
- import type { MutationCtx, QueryCtx } from "./_generated/server.js";
10
+ import type { ActionCtx, MutationCtx, QueryCtx } from "./_generated/server.js";
10
11
  import type { Id } from "./_generated/dataModel.js";
11
12
  import { computeRetryDelayMs, DEFAULT_CONFIG, providerConfigValidator } from "./config.js";
12
13
  import {
@@ -47,6 +48,15 @@ const telegramAttachmentValidator = v.object({
47
48
  mimeType: v.optional(v.string()),
48
49
  sizeBytes: v.optional(v.number()),
49
50
  expiresAt: v.number(),
51
+ downloadUrl: v.optional(v.string()),
52
+ });
53
+
54
+ const telegramAttachmentCandidateValidator = v.object({
55
+ kind: telegramAttachmentKindValidator,
56
+ telegramFileId: v.string(),
57
+ fileName: v.optional(v.string()),
58
+ mimeType: v.optional(v.string()),
59
+ sizeBytes: v.optional(v.number()),
50
60
  });
51
61
 
52
62
  const queuePayloadValidator = v.object({
@@ -220,6 +230,26 @@ const RUNTIME_CONFIG_KEYS = {
220
230
 
221
231
  const DEFAULT_TELEGRAM_ATTACHMENT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
222
232
 
233
+ type TelegramAttachmentCandidate = {
234
+ kind: "photo" | "video" | "audio" | "voice" | "document";
235
+ telegramFileId: string;
236
+ fileName?: string;
237
+ mimeType?: string;
238
+ sizeBytes?: number;
239
+ };
240
+
241
+ type PreparedTelegramAttachment = {
242
+ kind: "photo" | "video" | "audio" | "voice" | "document";
243
+ status: "ready";
244
+ storageId: Id<"_storage">;
245
+ telegramFileId: string;
246
+ fileName?: string;
247
+ mimeType?: string;
248
+ sizeBytes?: number;
249
+ expiresAt: number;
250
+ downloadUrl?: string;
251
+ };
252
+
223
253
  export const enqueueMessage = mutation({
224
254
  args: {
225
255
  conversationId: v.string(),
@@ -588,6 +618,39 @@ export const getTelegramIngressRuntimeConfig = internalQuery({
588
618
  },
589
619
  });
590
620
 
621
+ export const prepareTelegramAttachmentsForEnqueue = action({
622
+ args: {
623
+ agentKey: v.string(),
624
+ attachments: v.array(telegramAttachmentCandidateValidator),
625
+ },
626
+ returns: v.array(telegramAttachmentValidator),
627
+ handler: async (
628
+ ctx,
629
+ args,
630
+ ): Promise<Array<PreparedTelegramAttachment>> => {
631
+ const ingressConfig: {
632
+ botToken: string | null;
633
+ attachmentRetentionMs: number;
634
+ } = await ctx.runQuery(internal.queue.getTelegramIngressRuntimeConfig, {
635
+ agentKey: args.agentKey,
636
+ });
637
+ if (!ingressConfig.botToken) {
638
+ throw new Error(`missing active telegram bot token for agent '${args.agentKey}'`);
639
+ }
640
+ const expiresAt = Date.now() + ingressConfig.attachmentRetentionMs;
641
+ return await Promise.all(
642
+ args.attachments.map((attachment) =>
643
+ persistTelegramAttachmentFromCandidate(
644
+ ctx,
645
+ ingressConfig.botToken as string,
646
+ attachment,
647
+ expiresAt,
648
+ ),
649
+ ),
650
+ );
651
+ },
652
+ });
653
+
591
654
  export const upsertMessageRuntimeConfig = internalMutation({
592
655
  args: {
593
656
  messageConfig: messageRuntimeConfigValidator,
@@ -1756,12 +1819,13 @@ export const getHydrationBundleForClaimedJob = query({
1756
1819
  ? conversationCache.deltaContext
1757
1820
  : conversation.contextHistory;
1758
1821
  const bridgeRuntimeConfig = await resolveBridgeRuntimeConfig(ctx, profile);
1822
+ const hydratedPayload = await hydrateQueuePayloadAttachments(ctx, message.payload);
1759
1823
 
1760
1824
  return {
1761
1825
  messageId: message._id,
1762
1826
  conversationId: message.conversationId,
1763
1827
  agentKey: message.agentKey,
1764
- payload: message.payload,
1828
+ payload: hydratedPayload,
1765
1829
  conversationState: {
1766
1830
  contextHistory,
1767
1831
  pendingToolCalls: conversation.pendingToolCalls,
@@ -2834,6 +2898,46 @@ async function enqueueMessageRecord(
2834
2898
  return messageId;
2835
2899
  }
2836
2900
 
2901
+ async function hydrateQueuePayloadAttachments(
2902
+ ctx: QueryCtx,
2903
+ payload: {
2904
+ provider: string;
2905
+ providerUserId: string;
2906
+ messageText: string;
2907
+ externalMessageId?: string;
2908
+ rawUpdateJson?: string;
2909
+ metadata?: Record<string, string>;
2910
+ attachments?: Array<{
2911
+ kind: "photo" | "video" | "audio" | "voice" | "document";
2912
+ status: "ready" | "expired";
2913
+ storageId: Id<"_storage">;
2914
+ telegramFileId: string;
2915
+ fileName?: string;
2916
+ mimeType?: string;
2917
+ sizeBytes?: number;
2918
+ expiresAt: number;
2919
+ downloadUrl?: string;
2920
+ }>;
2921
+ },
2922
+ ) {
2923
+ if (!payload.attachments?.length) {
2924
+ return payload;
2925
+ }
2926
+ const attachments = await Promise.all(
2927
+ payload.attachments.map(async (attachment) => ({
2928
+ ...attachment,
2929
+ downloadUrl:
2930
+ attachment.status === "ready"
2931
+ ? ((await ctx.storage.getUrl(attachment.storageId)) ?? undefined)
2932
+ : undefined,
2933
+ })),
2934
+ );
2935
+ return {
2936
+ ...payload,
2937
+ attachments,
2938
+ };
2939
+ }
2940
+
2837
2941
  async function resolveConversationTargetForUserAgent(
2838
2942
  ctx: QueryCtx | MutationCtx,
2839
2943
  consumerUserId: string,
@@ -3152,6 +3256,159 @@ async function resolveActiveTelegramBotToken(
3152
3256
  return null;
3153
3257
  }
3154
3258
 
3259
+ async function persistTelegramAttachmentFromCandidate(
3260
+ ctx: Pick<ActionCtx, "runMutation">,
3261
+ telegramBotToken: string,
3262
+ attachment: TelegramAttachmentCandidate,
3263
+ expiresAt: number,
3264
+ ): Promise<PreparedTelegramAttachment> {
3265
+ const filePath = await fetchTelegramFilePath(telegramBotToken, attachment.telegramFileId);
3266
+ const downloaded = await downloadTelegramFile(telegramBotToken, filePath);
3267
+ const resolvedMimeType = resolvePreferredTelegramAttachmentMimeType(
3268
+ attachment.mimeType,
3269
+ attachment.fileName,
3270
+ downloaded.mimeType,
3271
+ filePath,
3272
+ );
3273
+ const uploadTarget: { uploadUrl: string } = await ctx.runMutation(
3274
+ api.queue.generateMediaUploadUrl,
3275
+ {},
3276
+ );
3277
+ const uploadResponse = await fetch(uploadTarget.uploadUrl, {
3278
+ method: "POST",
3279
+ headers:
3280
+ resolvedMimeType.length > 0
3281
+ ? {
3282
+ "Content-Type": resolvedMimeType,
3283
+ }
3284
+ : undefined,
3285
+ body: downloaded.blob,
3286
+ });
3287
+ const uploadPayload = (await uploadResponse.json().catch(() => ({}))) as {
3288
+ storageId?: Id<"_storage">;
3289
+ };
3290
+ if (!uploadResponse.ok || !uploadPayload.storageId) {
3291
+ throw new Error(`Convex storage upload failed for Telegram ${attachment.kind} attachment`);
3292
+ }
3293
+ return {
3294
+ kind: attachment.kind,
3295
+ status: "ready" as const,
3296
+ storageId: uploadPayload.storageId,
3297
+ telegramFileId: attachment.telegramFileId,
3298
+ fileName: attachment.fileName,
3299
+ mimeType: resolvedMimeType,
3300
+ sizeBytes: attachment.sizeBytes ?? downloaded.blob.size,
3301
+ expiresAt,
3302
+ };
3303
+ }
3304
+
3305
+ async function fetchTelegramFilePath(
3306
+ telegramBotToken: string,
3307
+ telegramFileId: string,
3308
+ ): Promise<string> {
3309
+ const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(telegramBotToken)}`;
3310
+ const response = await fetch(
3311
+ `${telegramApiBaseUrl}/getFile?file_id=${encodeURIComponent(telegramFileId)}`,
3312
+ );
3313
+ const payload = (await response.json().catch(() => ({}))) as {
3314
+ ok?: boolean;
3315
+ description?: string;
3316
+ result?: {
3317
+ file_path?: string;
3318
+ };
3319
+ };
3320
+ if (!response.ok || payload.ok !== true || typeof payload.result?.file_path !== "string") {
3321
+ throw new Error(
3322
+ `Telegram getFile failed: ${typeof payload.description === "string" ? payload.description : "missing file_path"}`,
3323
+ );
3324
+ }
3325
+ return payload.result.file_path;
3326
+ }
3327
+
3328
+ async function downloadTelegramFile(telegramBotToken: string, filePath: string) {
3329
+ const response = await fetch(
3330
+ `https://api.telegram.org/file/bot${encodeURIComponent(telegramBotToken)}/${filePath}`,
3331
+ );
3332
+ if (!response.ok) {
3333
+ throw new Error(`Telegram file download failed with status ${response.status}`);
3334
+ }
3335
+ const blob = await response.blob();
3336
+ const mimeType =
3337
+ response.headers.get("Content-Type") ??
3338
+ blob.type ??
3339
+ inferMimeTypeFromFilePath(filePath) ??
3340
+ "application/octet-stream";
3341
+ return {
3342
+ blob: mimeType === blob.type ? blob : new Blob([await blob.arrayBuffer()], { type: mimeType }),
3343
+ mimeType,
3344
+ };
3345
+ }
3346
+
3347
+ function resolvePreferredTelegramAttachmentMimeType(
3348
+ originalMimeType: string | undefined,
3349
+ fileName: string | undefined,
3350
+ downloadedMimeType: string,
3351
+ filePath: string,
3352
+ ): string {
3353
+ const normalizedOriginalMimeType = normalizeNonGenericMimeType(originalMimeType);
3354
+ if (normalizedOriginalMimeType) {
3355
+ return normalizedOriginalMimeType;
3356
+ }
3357
+ const inferredFromFileName = inferMimeTypeFromFileName(fileName);
3358
+ if (inferredFromFileName) {
3359
+ return inferredFromFileName;
3360
+ }
3361
+ const normalizedDownloadedMimeType = normalizeNonGenericMimeType(downloadedMimeType);
3362
+ if (normalizedDownloadedMimeType) {
3363
+ return normalizedDownloadedMimeType;
3364
+ }
3365
+ return inferMimeTypeFromFilePath(filePath) ?? "application/octet-stream";
3366
+ }
3367
+
3368
+ function normalizeNonGenericMimeType(mimeType?: string | null): string | null {
3369
+ if (typeof mimeType !== "string") {
3370
+ return null;
3371
+ }
3372
+ const normalizedMimeType = mimeType.trim().toLowerCase();
3373
+ if (!normalizedMimeType || normalizedMimeType === "application/octet-stream") {
3374
+ return null;
3375
+ }
3376
+ return normalizedMimeType;
3377
+ }
3378
+
3379
+ function inferMimeTypeFromFileName(fileName?: string | null): string | null {
3380
+ if (typeof fileName !== "string") {
3381
+ return null;
3382
+ }
3383
+ return inferMimeTypeFromFilePath(fileName);
3384
+ }
3385
+
3386
+ function inferMimeTypeFromFilePath(filePath: string): string | null {
3387
+ const normalizedPath = filePath.toLowerCase();
3388
+ if (normalizedPath.endsWith(".jpg") || normalizedPath.endsWith(".jpeg")) {
3389
+ return "image/jpeg";
3390
+ }
3391
+ if (normalizedPath.endsWith(".png")) {
3392
+ return "image/png";
3393
+ }
3394
+ if (normalizedPath.endsWith(".webp")) {
3395
+ return "image/webp";
3396
+ }
3397
+ if (normalizedPath.endsWith(".mp4")) {
3398
+ return "video/mp4";
3399
+ }
3400
+ if (normalizedPath.endsWith(".mp3")) {
3401
+ return "audio/mpeg";
3402
+ }
3403
+ if (normalizedPath.endsWith(".ogg")) {
3404
+ return "audio/ogg";
3405
+ }
3406
+ if (normalizedPath.endsWith(".pdf")) {
3407
+ return "application/pdf";
3408
+ }
3409
+ return null;
3410
+ }
3411
+
3155
3412
  async function buildGlobalSkillMaterialization(skill: {
3156
3413
  slug: string;
3157
3414
  version: string;
@@ -89,6 +89,7 @@ export default defineSchema({
89
89
  mimeType: v.optional(v.string()),
90
90
  sizeBytes: v.optional(v.number()),
91
91
  expiresAt: v.number(),
92
+ downloadUrl: v.optional(v.string()),
92
93
  }),
93
94
  ),
94
95
  ),