@okrlinkhub/agent-factory 3.0.0 → 3.0.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.
Files changed (35) hide show
  1. package/dist/client/index.d.ts +42 -14
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +68 -2
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/component/_generated/api.d.ts +2 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -1
  7. package/dist/component/_generated/api.js.map +1 -1
  8. package/dist/component/_generated/component.d.ts +114 -0
  9. package/dist/component/_generated/component.d.ts.map +1 -1
  10. package/dist/component/lib.d.ts +2 -1
  11. package/dist/component/lib.d.ts.map +1 -1
  12. package/dist/component/lib.js +2 -1
  13. package/dist/component/lib.js.map +1 -1
  14. package/dist/component/messageTemplates.d.ts +37 -0
  15. package/dist/component/messageTemplates.d.ts.map +1 -0
  16. package/dist/component/messageTemplates.js +177 -0
  17. package/dist/component/messageTemplates.js.map +1 -0
  18. package/dist/component/pushing.d.ts +14 -14
  19. package/dist/component/queue.d.ts +20 -0
  20. package/dist/component/queue.d.ts.map +1 -1
  21. package/dist/component/queue.js +51 -0
  22. package/dist/component/queue.js.map +1 -1
  23. package/dist/component/schema.d.ts +34 -8
  24. package/dist/component/schema.d.ts.map +1 -1
  25. package/dist/component/schema.js +14 -0
  26. package/dist/component/schema.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/client/index.ts +69 -2
  29. package/src/component/_generated/api.ts +2 -0
  30. package/src/component/_generated/component.ts +158 -0
  31. package/src/component/lib.test.ts +125 -0
  32. package/src/component/lib.ts +8 -0
  33. package/src/component/messageTemplates.ts +205 -0
  34. package/src/component/queue.ts +61 -0
  35. package/src/component/schema.ts +15 -0
@@ -201,6 +201,131 @@ describe("component lib", () => {
201
201
  expect(claim?.payload.messageText).toBe("hello");
202
202
  });
203
203
 
204
+ test("message templates should normalize tags and auto-generate key", async () => {
205
+ const t = initConvexTest();
206
+
207
+ const templateId = await t.mutation(api.lib.createMessageTemplate, {
208
+ title: " Status Update ",
209
+ text: " Share your latest progress. ",
210
+ tags: [" Urgent ", "follow-up", "urgent", " Team "],
211
+ actorUserId: "user-admin-1",
212
+ });
213
+
214
+ expect(templateId).toBeDefined();
215
+
216
+ const templates = await t.query(api.lib.listMessageTemplatesByCompany, {
217
+ includeDisabled: true,
218
+ });
219
+
220
+ expect(templates).toHaveLength(1);
221
+ expect(templates[0]).toMatchObject({
222
+ templateKey: "status-update",
223
+ title: "Status Update",
224
+ text: "Share your latest progress.",
225
+ tags: ["follow-up", "team", "urgent"],
226
+ usageCount: 0,
227
+ enabled: true,
228
+ });
229
+ });
230
+
231
+ test("message templates should generate a unique key globally", async () => {
232
+ const t = initConvexTest();
233
+
234
+ const firstTemplateId = await t.mutation(api.lib.createMessageTemplate, {
235
+ title: "Daily check-in",
236
+ text: "Share your update.",
237
+ tags: [],
238
+ actorUserId: "user-admin-1",
239
+ });
240
+
241
+ const secondTemplateId = await t.mutation(api.lib.createMessageTemplate, {
242
+ title: "Daily check-in",
243
+ text: "Duplicate title",
244
+ tags: [],
245
+ actorUserId: "user-admin-2",
246
+ });
247
+
248
+ const templates = await t.query(api.lib.listMessageTemplatesByCompany, {
249
+ includeDisabled: true,
250
+ });
251
+
252
+ expect(firstTemplateId).toBeDefined();
253
+ expect(secondTemplateId).toBeDefined();
254
+ expect(templates.map((template) => template.templateKey).sort()).toEqual([
255
+ "daily-check-in",
256
+ "daily-check-in-2",
257
+ ]);
258
+ });
259
+
260
+ test("message templates should track usage and surface most used templates first", async () => {
261
+ const t = initConvexTest();
262
+ await t.mutation(api.queue.upsertAgentProfile, {
263
+ agentKey: "template-agent",
264
+ version: "1.0.0",
265
+ secretsRef: [],
266
+ enabled: true,
267
+ });
268
+ await t.mutation(api.lib.bindUserAgent, {
269
+ consumerUserId: "user-template-1",
270
+ agentKey: "template-agent",
271
+ source: "manual",
272
+ metadata: { companyId: "co-templates" },
273
+ });
274
+
275
+ const firstTemplateId = await t.mutation(api.lib.createMessageTemplate, {
276
+ title: "Weekly recap",
277
+ text: "Condividi il recap della settimana.",
278
+ tags: ["weekly", "recap"],
279
+ actorUserId: "user-admin-1",
280
+ });
281
+ const secondTemplateId = await t.mutation(api.lib.createMessageTemplate, {
282
+ title: "Quick nudge",
283
+ text: "Mandami un aggiornamento rapido.",
284
+ tags: ["follow-up"],
285
+ actorUserId: "user-admin-1",
286
+ });
287
+
288
+ await t.mutation((api.lib as any).sendMessageTemplateToUserAgent, {
289
+ consumerUserId: "user-template-1",
290
+ agentKey: "template-agent",
291
+ templateId: firstTemplateId,
292
+ });
293
+ await t.mutation((api.lib as any).sendMessageTemplateToUserAgent, {
294
+ consumerUserId: "user-template-1",
295
+ agentKey: "template-agent",
296
+ templateId: firstTemplateId,
297
+ });
298
+ await t.mutation((api.lib as any).sendMessageTemplateToUserAgent, {
299
+ consumerUserId: "user-template-1",
300
+ agentKey: "template-agent",
301
+ templateId: secondTemplateId,
302
+ });
303
+
304
+ const templates = await t.query(api.lib.listMessageTemplatesByCompany, {
305
+ includeDisabled: true,
306
+ });
307
+
308
+ expect(templates.map((template) => ({
309
+ title: template.title,
310
+ usageCount: template.usageCount,
311
+ }))).toEqual([
312
+ { title: "Weekly recap", usageCount: 2 },
313
+ { title: "Quick nudge", usageCount: 1 },
314
+ ]);
315
+
316
+ const queuedItems = await t.query(api.lib.listQueueItemsForUserAgent, {
317
+ consumerUserId: "user-template-1",
318
+ agentKey: "template-agent",
319
+ limit: 10,
320
+ });
321
+ expect(queuedItems).toHaveLength(3);
322
+ expect(queuedItems.map((item) => item.payload.messageText).sort()).toEqual([
323
+ "Condividi il recap della settimana.",
324
+ "Condividi il recap della settimana.",
325
+ "Mandami un aggiornamento rapido.",
326
+ ].sort());
327
+ });
328
+
204
329
  test("message runtime config should store telegram attachment retention", async () => {
205
330
  const t = initConvexTest();
206
331
  await t.mutation(api.lib.setMessageRuntimeConfig, {
@@ -23,6 +23,7 @@ export {
23
23
  listQueueItemsForUserAgent,
24
24
  getConversationViewForUserAgent,
25
25
  sendMessageToUserAgent,
26
+ sendMessageTemplateToUserAgent,
26
27
  listSnapshotsForConversation,
27
28
  listSnapshotsForUserAgent,
28
29
  getLatestSnapshotForUserAgent,
@@ -59,6 +60,13 @@ export {
59
60
  getWebhookReadiness,
60
61
  } from "./identity.js";
61
62
 
63
+ export {
64
+ createMessageTemplate,
65
+ updateMessageTemplate,
66
+ deleteMessageTemplate,
67
+ listMessageTemplatesByCompany,
68
+ } from "./messageTemplates.js";
69
+
62
70
  export {
63
71
  createPushTemplate,
64
72
  updatePushTemplate,
@@ -0,0 +1,205 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server.js";
3
+ import type { MutationCtx } from "./_generated/server.js";
4
+
5
+ const messageTemplateViewValidator = v.object({
6
+ _id: v.id("messageTemplates"),
7
+ templateKey: v.string(),
8
+ title: v.string(),
9
+ text: v.string(),
10
+ tags: v.array(v.string()),
11
+ usageCount: v.number(),
12
+ enabled: v.boolean(),
13
+ createdBy: v.string(),
14
+ updatedBy: v.string(),
15
+ createdAt: v.number(),
16
+ updatedAt: v.number(),
17
+ });
18
+
19
+ function normalizeTemplateKey(value: string) {
20
+ return value.trim().toLowerCase();
21
+ }
22
+
23
+ function buildTemplateKeyBase(title: string) {
24
+ const normalized = title
25
+ .trim()
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, "-")
28
+ .replace(/^-+|-+$/g, "");
29
+ return normalized || "message-template";
30
+ }
31
+
32
+ function normalizeTitle(value: string) {
33
+ return value.trim();
34
+ }
35
+
36
+ function normalizeText(value: string) {
37
+ return value.trim();
38
+ }
39
+
40
+ function normalizeTags(tags: Array<string>) {
41
+ return Array.from(
42
+ new Set(
43
+ tags
44
+ .map((tag) => tag.trim().toLowerCase())
45
+ .filter((tag) => tag.length > 0),
46
+ ),
47
+ ).sort((left, right) => left.localeCompare(right));
48
+ }
49
+
50
+ function validateMessageTemplateFields(input: {
51
+ title?: string;
52
+ text?: string;
53
+ }) {
54
+ if (input.title !== undefined && input.title.length === 0) {
55
+ throw new Error("Template title is required");
56
+ }
57
+ if (input.text !== undefined && input.text.length === 0) {
58
+ throw new Error("Template text is required");
59
+ }
60
+ }
61
+
62
+ async function resolveUniqueTemplateKey(
63
+ ctx: MutationCtx,
64
+ title: string,
65
+ currentTemplateId?: string,
66
+ ) {
67
+ const baseKey = buildTemplateKeyBase(title);
68
+ let candidate = baseKey;
69
+ let suffix = 2;
70
+
71
+ while (true) {
72
+ const existing = await ctx.db
73
+ .query("messageTemplates")
74
+ .withIndex("by_templateKey", (q) => q.eq("templateKey", candidate))
75
+ .unique();
76
+ if (!existing || String(existing._id) === currentTemplateId) {
77
+ return candidate;
78
+ }
79
+ candidate = `${baseKey}-${suffix}`;
80
+ suffix += 1;
81
+ }
82
+ }
83
+
84
+ export const createMessageTemplate = mutation({
85
+ args: {
86
+ title: v.string(),
87
+ text: v.string(),
88
+ tags: v.array(v.string()),
89
+ enabled: v.optional(v.boolean()),
90
+ actorUserId: v.string(),
91
+ nowMs: v.optional(v.number()),
92
+ },
93
+ returns: v.id("messageTemplates"),
94
+ handler: async (ctx, args) => {
95
+ const title = normalizeTitle(args.title);
96
+ const text = normalizeText(args.text);
97
+ const tags = normalizeTags(args.tags);
98
+ validateMessageTemplateFields({ title, text });
99
+ const templateKey = normalizeTemplateKey(await resolveUniqueTemplateKey(ctx, title));
100
+
101
+ const nowMs = args.nowMs ?? Date.now();
102
+ return await ctx.db.insert("messageTemplates", {
103
+ templateKey,
104
+ title,
105
+ text,
106
+ tags,
107
+ usageCount: 0,
108
+ enabled: args.enabled ?? true,
109
+ createdBy: args.actorUserId,
110
+ updatedBy: args.actorUserId,
111
+ createdAt: nowMs,
112
+ updatedAt: nowMs,
113
+ });
114
+ },
115
+ });
116
+
117
+ export const updateMessageTemplate = mutation({
118
+ args: {
119
+ templateId: v.id("messageTemplates"),
120
+ title: v.optional(v.string()),
121
+ text: v.optional(v.string()),
122
+ tags: v.optional(v.array(v.string())),
123
+ enabled: v.optional(v.boolean()),
124
+ actorUserId: v.string(),
125
+ nowMs: v.optional(v.number()),
126
+ },
127
+ returns: v.boolean(),
128
+ handler: async (ctx, args) => {
129
+ const template = await ctx.db.get(args.templateId);
130
+ if (!template) return false;
131
+
132
+ const title = args.title !== undefined ? normalizeTitle(args.title) : template.title;
133
+ const text = args.text !== undefined ? normalizeText(args.text) : template.text;
134
+ const tags = args.tags !== undefined ? normalizeTags(args.tags) : template.tags;
135
+ validateMessageTemplateFields({ title, text });
136
+ const templateKey =
137
+ title !== template.title
138
+ ? normalizeTemplateKey(await resolveUniqueTemplateKey(ctx, title, String(template._id)))
139
+ : template.templateKey;
140
+
141
+ const nowMs = args.nowMs ?? Date.now();
142
+ await ctx.db.patch(template._id, {
143
+ templateKey,
144
+ title,
145
+ text,
146
+ tags,
147
+ enabled: args.enabled ?? template.enabled,
148
+ updatedBy: args.actorUserId,
149
+ updatedAt: nowMs,
150
+ });
151
+ return true;
152
+ },
153
+ });
154
+
155
+ export const deleteMessageTemplate = mutation({
156
+ args: {
157
+ templateId: v.id("messageTemplates"),
158
+ },
159
+ returns: v.boolean(),
160
+ handler: async (ctx, args) => {
161
+ const template = await ctx.db.get(args.templateId);
162
+ if (!template) return false;
163
+ await ctx.db.delete(template._id);
164
+ return true;
165
+ },
166
+ });
167
+
168
+ export const listMessageTemplatesByCompany = query({
169
+ args: {
170
+ includeDisabled: v.optional(v.boolean()),
171
+ limit: v.optional(v.number()),
172
+ },
173
+ returns: v.array(messageTemplateViewValidator),
174
+ handler: async (ctx, args) => {
175
+ const includeDisabled = args.includeDisabled ?? false;
176
+ const limit = Math.max(1, Math.min(args.limit ?? 100, 200));
177
+
178
+ const rows = includeDisabled
179
+ ? await ctx.db.query("messageTemplates").take(limit)
180
+ : await ctx.db
181
+ .query("messageTemplates")
182
+ .withIndex("by_enabled", (q) => q.eq("enabled", true))
183
+ .take(limit);
184
+
185
+ rows.sort((left, right) => {
186
+ if (right.usageCount !== left.usageCount) {
187
+ return right.usageCount - left.usageCount;
188
+ }
189
+ return right.updatedAt - left.updatedAt;
190
+ });
191
+ return rows.map((row) => ({
192
+ _id: row._id,
193
+ templateKey: row.templateKey,
194
+ title: row.title,
195
+ text: row.text,
196
+ tags: row.tags,
197
+ usageCount: row.usageCount,
198
+ enabled: row.enabled,
199
+ createdBy: row.createdBy,
200
+ updatedBy: row.updatedBy,
201
+ createdAt: row.createdAt,
202
+ updatedAt: row.updatedAt,
203
+ }));
204
+ },
205
+ });
@@ -2013,6 +2013,67 @@ export const sendMessageToUserAgent = mutation({
2013
2013
  },
2014
2014
  });
2015
2015
 
2016
+ export const sendMessageTemplateToUserAgent = mutation({
2017
+ args: {
2018
+ consumerUserId: v.string(),
2019
+ agentKey: v.string(),
2020
+ templateId: v.id("messageTemplates"),
2021
+ metadata: v.optional(v.record(v.string(), v.string())),
2022
+ nowMs: v.optional(v.number()),
2023
+ providerConfig: v.optional(providerConfigValidator),
2024
+ },
2025
+ returns: v.object({
2026
+ messageId: v.id("messageQueue"),
2027
+ usageCount: v.number(),
2028
+ }),
2029
+ handler: async (ctx, args) => {
2030
+ const template = await ctx.db.get(args.templateId);
2031
+ if (!template || !template.enabled) {
2032
+ throw new Error("Message template not found");
2033
+ }
2034
+
2035
+ const nowMs = args.nowMs ?? Date.now();
2036
+ const target = await resolveConversationTargetForUserAgent(
2037
+ ctx,
2038
+ args.consumerUserId,
2039
+ args.agentKey,
2040
+ true,
2041
+ );
2042
+ const providerUserId =
2043
+ target.telegramUserId ?? target.telegramChatId ?? args.consumerUserId;
2044
+ const usageCount = template.usageCount + 1;
2045
+ const messageId = await enqueueMessageRecord(ctx, {
2046
+ conversationId: target.conversationId,
2047
+ agentKey: args.agentKey,
2048
+ payload: {
2049
+ provider: target.provider,
2050
+ providerUserId,
2051
+ messageText: template.text,
2052
+ metadata: {
2053
+ ...(args.metadata ?? {}),
2054
+ consumerUserId: args.consumerUserId,
2055
+ source: "message_template",
2056
+ templateId: String(template._id),
2057
+ templateKey: template.templateKey,
2058
+ ...(target.telegramChatId ? { telegramChatId: target.telegramChatId } : {}),
2059
+ ...(target.telegramUserId ? { telegramUserId: target.telegramUserId } : {}),
2060
+ },
2061
+ },
2062
+ scheduledFor: nowMs,
2063
+ providerConfig: args.providerConfig,
2064
+ });
2065
+
2066
+ await ctx.db.patch(template._id, {
2067
+ usageCount,
2068
+ });
2069
+
2070
+ return {
2071
+ messageId,
2072
+ usageCount,
2073
+ };
2074
+ },
2075
+ });
2076
+
2016
2077
  export const listSnapshotsForConversation = query({
2017
2078
  args: {
2018
2079
  conversationId: v.string(),
@@ -410,6 +410,21 @@ export default defineSchema({
410
410
  .index("by_companyId_and_templateKey", ["companyId", "templateKey"])
411
411
  .index("by_companyId_and_enabled", ["companyId", "enabled"]),
412
412
 
413
+ messageTemplates: defineTable({
414
+ templateKey: v.string(),
415
+ title: v.string(),
416
+ text: v.string(),
417
+ tags: v.array(v.string()),
418
+ usageCount: v.number(),
419
+ enabled: v.boolean(),
420
+ createdBy: v.string(),
421
+ updatedBy: v.string(),
422
+ createdAt: v.number(),
423
+ updatedAt: v.number(),
424
+ })
425
+ .index("by_templateKey", ["templateKey"])
426
+ .index("by_enabled", ["enabled"]),
427
+
413
428
  messagePushJobs: defineTable({
414
429
  companyId: v.string(),
415
430
  consumerUserId: v.string(),