@objectstack/service-ai 6.8.1 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +170 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +75 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +170 -6
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.d.cts
CHANGED
|
@@ -308,6 +308,20 @@ declare class AIService implements IAIService {
|
|
|
308
308
|
private readonly pendingDispatchers;
|
|
309
309
|
/** Data engine for `ai_pending_actions` persistence. */
|
|
310
310
|
private readonly dataEngine?;
|
|
311
|
+
/**
|
|
312
|
+
* Auto-title configuration. When `enabled`, the first `chatWithTools` /
|
|
313
|
+
* `streamChatWithTools` call against a still-untitled conversation
|
|
314
|
+
* triggers a one-shot LLM call (fire-and-forget) that summarises the
|
|
315
|
+
* exchange into a short title and PATCHes it onto the conversation row.
|
|
316
|
+
*
|
|
317
|
+
* Defaults to disabled — `AIServicePlugin` flips this on (with values
|
|
318
|
+
* read from the `ai` settings namespace) once the kernel is ready.
|
|
319
|
+
* Keeping the default off means unit tests don't accidentally make
|
|
320
|
+
* extra adapter calls.
|
|
321
|
+
*/
|
|
322
|
+
private titleGeneration;
|
|
323
|
+
/** Tracks conversations we've already attempted to title to avoid duplicate LLM calls. */
|
|
324
|
+
private readonly titledConversations;
|
|
311
325
|
constructor(config?: AIServiceConfig);
|
|
312
326
|
/** The name of the active LLM adapter. */
|
|
313
327
|
get adapterName(): string;
|
|
@@ -318,6 +332,36 @@ declare class AIService implements IAIService {
|
|
|
318
332
|
* subsequent calls go through the new adapter.
|
|
319
333
|
*/
|
|
320
334
|
setAdapter(next: LLMAdapter): void;
|
|
335
|
+
/**
|
|
336
|
+
* Configure conversation auto-titling. Called by `AIServicePlugin`
|
|
337
|
+
* when the `ai` settings namespace is bound (so admins can toggle
|
|
338
|
+
* the feature live from the Setup app without a restart).
|
|
339
|
+
*
|
|
340
|
+
* - `enabled=false` is the safe default for unit tests and the
|
|
341
|
+
* memory adapter (which would just echo the prompt back as a title).
|
|
342
|
+
* - `maxLength` is enforced both in the prompt and as a hard server-side
|
|
343
|
+
* `slice()` so a misbehaving model can't write a 4 KB "title".
|
|
344
|
+
*/
|
|
345
|
+
setTitleGenerationConfig(config: {
|
|
346
|
+
enabled: boolean;
|
|
347
|
+
maxLength?: number;
|
|
348
|
+
}): void;
|
|
349
|
+
/**
|
|
350
|
+
* Best-effort title generation for a conversation. Idempotent per
|
|
351
|
+
* `AIService` instance — once attempted, the id is recorded in
|
|
352
|
+
* `titledConversations` so subsequent chats don't burn extra tokens
|
|
353
|
+
* re-summarising the same thread.
|
|
354
|
+
*
|
|
355
|
+
* Skips when:
|
|
356
|
+
* - feature disabled
|
|
357
|
+
* - conversation already has a non-empty title
|
|
358
|
+
* - conversation has fewer than 2 messages (no exchange to summarise)
|
|
359
|
+
* - conversation has no user message
|
|
360
|
+
*
|
|
361
|
+
* Failures are logged at debug level and swallowed — title generation
|
|
362
|
+
* is purely cosmetic and must never break chat.
|
|
363
|
+
*/
|
|
364
|
+
summarizeConversation(conversationId: string): Promise<void>;
|
|
321
365
|
/**
|
|
322
366
|
* Best-effort auto-creation of a conversation when the caller did not
|
|
323
367
|
* supply one but did supply an actor we can attribute the chat to.
|
|
@@ -2258,6 +2302,14 @@ declare const AiConversationObject: Omit<{
|
|
|
2258
2302
|
} | undefined;
|
|
2259
2303
|
recordTypes?: string[] | undefined;
|
|
2260
2304
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
|
2305
|
+
publicSharing?: {
|
|
2306
|
+
enabled: boolean;
|
|
2307
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
2308
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
2309
|
+
maxExpiryDays?: number | undefined;
|
|
2310
|
+
redactFields?: string[] | undefined;
|
|
2311
|
+
eligibility?: string | undefined;
|
|
2312
|
+
} | undefined;
|
|
2261
2313
|
keyPrefix?: string | undefined;
|
|
2262
2314
|
detail?: {
|
|
2263
2315
|
[x: string]: unknown;
|
|
@@ -2348,6 +2400,13 @@ declare const AiConversationObject: Omit<{
|
|
|
2348
2400
|
readonly icon: "message-square";
|
|
2349
2401
|
readonly isSystem: true;
|
|
2350
2402
|
readonly description: "Persistent AI conversation metadata";
|
|
2403
|
+
readonly publicSharing: {
|
|
2404
|
+
readonly enabled: true;
|
|
2405
|
+
readonly allowedAudiences: ["link_only", "signed_in"];
|
|
2406
|
+
readonly allowedPermissions: ["view"];
|
|
2407
|
+
readonly maxExpiryDays: 90;
|
|
2408
|
+
readonly redactFields: ["metadata"];
|
|
2409
|
+
};
|
|
2351
2410
|
readonly fields: {
|
|
2352
2411
|
readonly id: {
|
|
2353
2412
|
readonly readonly?: boolean | undefined;
|
|
@@ -4187,6 +4246,14 @@ declare const AiMessageObject: Omit<{
|
|
|
4187
4246
|
} | undefined;
|
|
4188
4247
|
recordTypes?: string[] | undefined;
|
|
4189
4248
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
|
4249
|
+
publicSharing?: {
|
|
4250
|
+
enabled: boolean;
|
|
4251
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
4252
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
4253
|
+
maxExpiryDays?: number | undefined;
|
|
4254
|
+
redactFields?: string[] | undefined;
|
|
4255
|
+
eligibility?: string | undefined;
|
|
4256
|
+
} | undefined;
|
|
4190
4257
|
keyPrefix?: string | undefined;
|
|
4191
4258
|
detail?: {
|
|
4192
4259
|
[x: string]: unknown;
|
|
@@ -6118,6 +6185,14 @@ declare const AiTraceObject: Omit<{
|
|
|
6118
6185
|
} | undefined;
|
|
6119
6186
|
recordTypes?: string[] | undefined;
|
|
6120
6187
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
|
6188
|
+
publicSharing?: {
|
|
6189
|
+
enabled: boolean;
|
|
6190
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
6191
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
6192
|
+
maxExpiryDays?: number | undefined;
|
|
6193
|
+
redactFields?: string[] | undefined;
|
|
6194
|
+
eligibility?: string | undefined;
|
|
6195
|
+
} | undefined;
|
|
6121
6196
|
keyPrefix?: string | undefined;
|
|
6122
6197
|
detail?: {
|
|
6123
6198
|
[x: string]: unknown;
|
package/dist/index.d.ts
CHANGED
|
@@ -308,6 +308,20 @@ declare class AIService implements IAIService {
|
|
|
308
308
|
private readonly pendingDispatchers;
|
|
309
309
|
/** Data engine for `ai_pending_actions` persistence. */
|
|
310
310
|
private readonly dataEngine?;
|
|
311
|
+
/**
|
|
312
|
+
* Auto-title configuration. When `enabled`, the first `chatWithTools` /
|
|
313
|
+
* `streamChatWithTools` call against a still-untitled conversation
|
|
314
|
+
* triggers a one-shot LLM call (fire-and-forget) that summarises the
|
|
315
|
+
* exchange into a short title and PATCHes it onto the conversation row.
|
|
316
|
+
*
|
|
317
|
+
* Defaults to disabled — `AIServicePlugin` flips this on (with values
|
|
318
|
+
* read from the `ai` settings namespace) once the kernel is ready.
|
|
319
|
+
* Keeping the default off means unit tests don't accidentally make
|
|
320
|
+
* extra adapter calls.
|
|
321
|
+
*/
|
|
322
|
+
private titleGeneration;
|
|
323
|
+
/** Tracks conversations we've already attempted to title to avoid duplicate LLM calls. */
|
|
324
|
+
private readonly titledConversations;
|
|
311
325
|
constructor(config?: AIServiceConfig);
|
|
312
326
|
/** The name of the active LLM adapter. */
|
|
313
327
|
get adapterName(): string;
|
|
@@ -318,6 +332,36 @@ declare class AIService implements IAIService {
|
|
|
318
332
|
* subsequent calls go through the new adapter.
|
|
319
333
|
*/
|
|
320
334
|
setAdapter(next: LLMAdapter): void;
|
|
335
|
+
/**
|
|
336
|
+
* Configure conversation auto-titling. Called by `AIServicePlugin`
|
|
337
|
+
* when the `ai` settings namespace is bound (so admins can toggle
|
|
338
|
+
* the feature live from the Setup app without a restart).
|
|
339
|
+
*
|
|
340
|
+
* - `enabled=false` is the safe default for unit tests and the
|
|
341
|
+
* memory adapter (which would just echo the prompt back as a title).
|
|
342
|
+
* - `maxLength` is enforced both in the prompt and as a hard server-side
|
|
343
|
+
* `slice()` so a misbehaving model can't write a 4 KB "title".
|
|
344
|
+
*/
|
|
345
|
+
setTitleGenerationConfig(config: {
|
|
346
|
+
enabled: boolean;
|
|
347
|
+
maxLength?: number;
|
|
348
|
+
}): void;
|
|
349
|
+
/**
|
|
350
|
+
* Best-effort title generation for a conversation. Idempotent per
|
|
351
|
+
* `AIService` instance — once attempted, the id is recorded in
|
|
352
|
+
* `titledConversations` so subsequent chats don't burn extra tokens
|
|
353
|
+
* re-summarising the same thread.
|
|
354
|
+
*
|
|
355
|
+
* Skips when:
|
|
356
|
+
* - feature disabled
|
|
357
|
+
* - conversation already has a non-empty title
|
|
358
|
+
* - conversation has fewer than 2 messages (no exchange to summarise)
|
|
359
|
+
* - conversation has no user message
|
|
360
|
+
*
|
|
361
|
+
* Failures are logged at debug level and swallowed — title generation
|
|
362
|
+
* is purely cosmetic and must never break chat.
|
|
363
|
+
*/
|
|
364
|
+
summarizeConversation(conversationId: string): Promise<void>;
|
|
321
365
|
/**
|
|
322
366
|
* Best-effort auto-creation of a conversation when the caller did not
|
|
323
367
|
* supply one but did supply an actor we can attribute the chat to.
|
|
@@ -2258,6 +2302,14 @@ declare const AiConversationObject: Omit<{
|
|
|
2258
2302
|
} | undefined;
|
|
2259
2303
|
recordTypes?: string[] | undefined;
|
|
2260
2304
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
|
2305
|
+
publicSharing?: {
|
|
2306
|
+
enabled: boolean;
|
|
2307
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
2308
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
2309
|
+
maxExpiryDays?: number | undefined;
|
|
2310
|
+
redactFields?: string[] | undefined;
|
|
2311
|
+
eligibility?: string | undefined;
|
|
2312
|
+
} | undefined;
|
|
2261
2313
|
keyPrefix?: string | undefined;
|
|
2262
2314
|
detail?: {
|
|
2263
2315
|
[x: string]: unknown;
|
|
@@ -2348,6 +2400,13 @@ declare const AiConversationObject: Omit<{
|
|
|
2348
2400
|
readonly icon: "message-square";
|
|
2349
2401
|
readonly isSystem: true;
|
|
2350
2402
|
readonly description: "Persistent AI conversation metadata";
|
|
2403
|
+
readonly publicSharing: {
|
|
2404
|
+
readonly enabled: true;
|
|
2405
|
+
readonly allowedAudiences: ["link_only", "signed_in"];
|
|
2406
|
+
readonly allowedPermissions: ["view"];
|
|
2407
|
+
readonly maxExpiryDays: 90;
|
|
2408
|
+
readonly redactFields: ["metadata"];
|
|
2409
|
+
};
|
|
2351
2410
|
readonly fields: {
|
|
2352
2411
|
readonly id: {
|
|
2353
2412
|
readonly readonly?: boolean | undefined;
|
|
@@ -4187,6 +4246,14 @@ declare const AiMessageObject: Omit<{
|
|
|
4187
4246
|
} | undefined;
|
|
4188
4247
|
recordTypes?: string[] | undefined;
|
|
4189
4248
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
|
4249
|
+
publicSharing?: {
|
|
4250
|
+
enabled: boolean;
|
|
4251
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
4252
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
4253
|
+
maxExpiryDays?: number | undefined;
|
|
4254
|
+
redactFields?: string[] | undefined;
|
|
4255
|
+
eligibility?: string | undefined;
|
|
4256
|
+
} | undefined;
|
|
4190
4257
|
keyPrefix?: string | undefined;
|
|
4191
4258
|
detail?: {
|
|
4192
4259
|
[x: string]: unknown;
|
|
@@ -6118,6 +6185,14 @@ declare const AiTraceObject: Omit<{
|
|
|
6118
6185
|
} | undefined;
|
|
6119
6186
|
recordTypes?: string[] | undefined;
|
|
6120
6187
|
sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
|
|
6188
|
+
publicSharing?: {
|
|
6189
|
+
enabled: boolean;
|
|
6190
|
+
allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
|
|
6191
|
+
allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
|
|
6192
|
+
maxExpiryDays?: number | undefined;
|
|
6193
|
+
redactFields?: string[] | undefined;
|
|
6194
|
+
eligibility?: string | undefined;
|
|
6195
|
+
} | undefined;
|
|
6121
6196
|
keyPrefix?: string | undefined;
|
|
6122
6197
|
detail?: {
|
|
6123
6198
|
[x: string]: unknown;
|
package/dist/index.js
CHANGED
|
@@ -1450,6 +1450,36 @@ function finishPart(result) {
|
|
|
1450
1450
|
rawFinishReason: "stop"
|
|
1451
1451
|
};
|
|
1452
1452
|
}
|
|
1453
|
+
function extractMessageText(message) {
|
|
1454
|
+
const c = message.content;
|
|
1455
|
+
if (typeof c === "string") return c;
|
|
1456
|
+
if (!Array.isArray(c)) return "";
|
|
1457
|
+
const parts = [];
|
|
1458
|
+
for (const part of c) {
|
|
1459
|
+
if (typeof part === "string") {
|
|
1460
|
+
parts.push(part);
|
|
1461
|
+
} else if (part && typeof part === "object") {
|
|
1462
|
+
const p = part;
|
|
1463
|
+
if (p.type === "text" && typeof p.text === "string") parts.push(p.text);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return parts.join(" ").trim();
|
|
1467
|
+
}
|
|
1468
|
+
function cleanTitle(raw, maxLen) {
|
|
1469
|
+
let s = raw.replace(/\s+/g, " ").trim();
|
|
1470
|
+
s = s.replace(/^[\s"'“”‘’`「『((\[【]+/, "").replace(/[\s"'“”‘’`」』))\]】]+$/, "");
|
|
1471
|
+
s = s.replace(/^(title|标题|主题)\s*[::]\s*/i, "");
|
|
1472
|
+
s = s.replace(/^[\s"'“”‘’`「『((\[【]+/, "").replace(/[\s"'“”‘’`」』))\]】]+$/, "");
|
|
1473
|
+
s = s.replace(/[.。!!??,,;;::]+$/, "").trim();
|
|
1474
|
+
if (!s) return "";
|
|
1475
|
+
if (s.length <= maxLen) return s;
|
|
1476
|
+
if (/^[\x00-\x7F]+$/.test(s)) {
|
|
1477
|
+
const cut = s.slice(0, maxLen);
|
|
1478
|
+
const lastSpace = cut.lastIndexOf(" ");
|
|
1479
|
+
return lastSpace > maxLen / 2 ? cut.slice(0, lastSpace) : cut;
|
|
1480
|
+
}
|
|
1481
|
+
return s.slice(0, maxLen);
|
|
1482
|
+
}
|
|
1453
1483
|
var _AIService = class _AIService {
|
|
1454
1484
|
constructor(config = {}) {
|
|
1455
1485
|
/**
|
|
@@ -1459,6 +1489,23 @@ var _AIService = class _AIService {
|
|
|
1459
1489
|
* through `approvePendingAction()`.
|
|
1460
1490
|
*/
|
|
1461
1491
|
this.pendingDispatchers = /* @__PURE__ */ new Map();
|
|
1492
|
+
/**
|
|
1493
|
+
* Auto-title configuration. When `enabled`, the first `chatWithTools` /
|
|
1494
|
+
* `streamChatWithTools` call against a still-untitled conversation
|
|
1495
|
+
* triggers a one-shot LLM call (fire-and-forget) that summarises the
|
|
1496
|
+
* exchange into a short title and PATCHes it onto the conversation row.
|
|
1497
|
+
*
|
|
1498
|
+
* Defaults to disabled — `AIServicePlugin` flips this on (with values
|
|
1499
|
+
* read from the `ai` settings namespace) once the kernel is ready.
|
|
1500
|
+
* Keeping the default off means unit tests don't accidentally make
|
|
1501
|
+
* extra adapter calls.
|
|
1502
|
+
*/
|
|
1503
|
+
this.titleGeneration = {
|
|
1504
|
+
enabled: false,
|
|
1505
|
+
maxLength: 16
|
|
1506
|
+
};
|
|
1507
|
+
/** Tracks conversations we've already attempted to title to avoid duplicate LLM calls. */
|
|
1508
|
+
this.titledConversations = /* @__PURE__ */ new Set();
|
|
1462
1509
|
this.adapter = config.adapter ?? new MemoryLLMAdapter();
|
|
1463
1510
|
this.logger = config.logger ?? createLogger({ level: "info", format: "pretty" });
|
|
1464
1511
|
this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
|
|
@@ -1487,6 +1534,89 @@ var _AIService = class _AIService {
|
|
|
1487
1534
|
this.logger.info(`[AI] LLM adapter swapped: ${prev} \u2192 ${next.name}`);
|
|
1488
1535
|
}
|
|
1489
1536
|
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Configure conversation auto-titling. Called by `AIServicePlugin`
|
|
1539
|
+
* when the `ai` settings namespace is bound (so admins can toggle
|
|
1540
|
+
* the feature live from the Setup app without a restart).
|
|
1541
|
+
*
|
|
1542
|
+
* - `enabled=false` is the safe default for unit tests and the
|
|
1543
|
+
* memory adapter (which would just echo the prompt back as a title).
|
|
1544
|
+
* - `maxLength` is enforced both in the prompt and as a hard server-side
|
|
1545
|
+
* `slice()` so a misbehaving model can't write a 4 KB "title".
|
|
1546
|
+
*/
|
|
1547
|
+
setTitleGenerationConfig(config) {
|
|
1548
|
+
this.titleGeneration = {
|
|
1549
|
+
enabled: config.enabled,
|
|
1550
|
+
maxLength: Math.max(8, Math.min(80, config.maxLength ?? 16))
|
|
1551
|
+
};
|
|
1552
|
+
this.logger.debug("[AI] title generation config", this.titleGeneration);
|
|
1553
|
+
}
|
|
1554
|
+
/**
|
|
1555
|
+
* Best-effort title generation for a conversation. Idempotent per
|
|
1556
|
+
* `AIService` instance — once attempted, the id is recorded in
|
|
1557
|
+
* `titledConversations` so subsequent chats don't burn extra tokens
|
|
1558
|
+
* re-summarising the same thread.
|
|
1559
|
+
*
|
|
1560
|
+
* Skips when:
|
|
1561
|
+
* - feature disabled
|
|
1562
|
+
* - conversation already has a non-empty title
|
|
1563
|
+
* - conversation has fewer than 2 messages (no exchange to summarise)
|
|
1564
|
+
* - conversation has no user message
|
|
1565
|
+
*
|
|
1566
|
+
* Failures are logged at debug level and swallowed — title generation
|
|
1567
|
+
* is purely cosmetic and must never break chat.
|
|
1568
|
+
*/
|
|
1569
|
+
async summarizeConversation(conversationId) {
|
|
1570
|
+
if (!this.titleGeneration.enabled) return;
|
|
1571
|
+
if (this.titledConversations.has(conversationId)) return;
|
|
1572
|
+
this.titledConversations.add(conversationId);
|
|
1573
|
+
try {
|
|
1574
|
+
const conv = await this.conversationService.get(conversationId);
|
|
1575
|
+
if (!conv) return;
|
|
1576
|
+
if (conv.title && conv.title.trim().length > 0) return;
|
|
1577
|
+
if (!conv.messages || conv.messages.length < 2) return;
|
|
1578
|
+
const userMsg = conv.messages.find((m) => m.role === "user");
|
|
1579
|
+
const assistantMsg = conv.messages.find((m) => m.role === "assistant");
|
|
1580
|
+
if (!userMsg) return;
|
|
1581
|
+
const userText = extractMessageText(userMsg);
|
|
1582
|
+
const assistantText = assistantMsg ? extractMessageText(assistantMsg) : "";
|
|
1583
|
+
if (!userText) return;
|
|
1584
|
+
const maxLen = this.titleGeneration.maxLength;
|
|
1585
|
+
const prompt = [
|
|
1586
|
+
{
|
|
1587
|
+
role: "system",
|
|
1588
|
+
content: `You are a title generator. Produce a SHORT (<=${maxLen} characters), noun-phrase title that captures the topic of the conversation below. Reply with the title ONLY \u2014 no quotes, no punctuation, no preamble, no trailing period. Match the language of the user's message.`
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
role: "user",
|
|
1592
|
+
content: `User said:
|
|
1593
|
+
${userText.slice(0, 800)}` + (assistantText ? `
|
|
1594
|
+
|
|
1595
|
+
Assistant replied:
|
|
1596
|
+
${assistantText.slice(0, 800)}` : "")
|
|
1597
|
+
}
|
|
1598
|
+
];
|
|
1599
|
+
const result = await this.adapter.chat(prompt, {
|
|
1600
|
+
temperature: 0.3,
|
|
1601
|
+
maxTokens: 32
|
|
1602
|
+
});
|
|
1603
|
+
const raw = (result.content ?? "").trim();
|
|
1604
|
+
if (!raw) return;
|
|
1605
|
+
const cleaned = cleanTitle(raw, maxLen);
|
|
1606
|
+
if (!cleaned) return;
|
|
1607
|
+
await this.conversationService.update(conversationId, { title: cleaned });
|
|
1608
|
+
this.logger.debug("[AI] auto-titled conversation", {
|
|
1609
|
+
conversationId,
|
|
1610
|
+
title: cleaned
|
|
1611
|
+
});
|
|
1612
|
+
} catch (err) {
|
|
1613
|
+
this.titledConversations.delete(conversationId);
|
|
1614
|
+
this.logger.debug("[AI] summarizeConversation failed", {
|
|
1615
|
+
conversationId,
|
|
1616
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1490
1620
|
/**
|
|
1491
1621
|
* Best-effort auto-creation of a conversation when the caller did not
|
|
1492
1622
|
* supply one but did supply an actor we can attribute the chat to.
|
|
@@ -1687,6 +1817,7 @@ var _AIService = class _AIService {
|
|
|
1687
1817
|
role: "assistant",
|
|
1688
1818
|
content: result.content
|
|
1689
1819
|
});
|
|
1820
|
+
void this.summarizeConversation(conversationId);
|
|
1690
1821
|
}
|
|
1691
1822
|
return autoCreatedConversationId ? { ...result, conversationId: autoCreatedConversationId } : result;
|
|
1692
1823
|
}
|
|
@@ -1754,6 +1885,7 @@ var _AIService = class _AIService {
|
|
|
1754
1885
|
role: "assistant",
|
|
1755
1886
|
content: finalResult.content
|
|
1756
1887
|
});
|
|
1888
|
+
void this.summarizeConversation(conversationId);
|
|
1757
1889
|
}
|
|
1758
1890
|
return autoCreatedConversationId ? { ...finalResult, conversationId: autoCreatedConversationId } : finalResult;
|
|
1759
1891
|
}
|
|
@@ -1812,6 +1944,7 @@ var _AIService = class _AIService {
|
|
|
1812
1944
|
role: "assistant",
|
|
1813
1945
|
content: result2.content
|
|
1814
1946
|
});
|
|
1947
|
+
void this.summarizeConversation(conversationId);
|
|
1815
1948
|
}
|
|
1816
1949
|
yield textDeltaPart("stream", result2.content);
|
|
1817
1950
|
yield finishPart(result2);
|
|
@@ -1877,6 +2010,7 @@ var _AIService = class _AIService {
|
|
|
1877
2010
|
role: "assistant",
|
|
1878
2011
|
content: result.content
|
|
1879
2012
|
});
|
|
2013
|
+
void this.summarizeConversation(conversationId);
|
|
1880
2014
|
}
|
|
1881
2015
|
yield textDeltaPart("stream", result.content);
|
|
1882
2016
|
yield finishPart(result);
|
|
@@ -3363,6 +3497,19 @@ var AiConversationObject = ObjectSchema.create({
|
|
|
3363
3497
|
icon: "message-square",
|
|
3364
3498
|
isSystem: true,
|
|
3365
3499
|
description: "Persistent AI conversation metadata",
|
|
3500
|
+
// Enable Notion / Figma-style "anyone with the link" sharing.
|
|
3501
|
+
// The platform's plugin-sharing service exposes the share-link UI
|
|
3502
|
+
// and REST surface as soon as this flag is set; no further wiring
|
|
3503
|
+
// is needed in service-ai. `metadata` is redacted so internal
|
|
3504
|
+
// tracking payloads (model token counts, source app context) do not
|
|
3505
|
+
// leak into public shares.
|
|
3506
|
+
publicSharing: {
|
|
3507
|
+
enabled: true,
|
|
3508
|
+
allowedAudiences: ["link_only", "signed_in"],
|
|
3509
|
+
allowedPermissions: ["view"],
|
|
3510
|
+
maxExpiryDays: 90,
|
|
3511
|
+
redactFields: ["metadata"]
|
|
3512
|
+
},
|
|
3366
3513
|
fields: {
|
|
3367
3514
|
id: Field.text({
|
|
3368
3515
|
label: "Conversation ID",
|
|
@@ -5552,9 +5699,9 @@ var AIServicePlugin = class {
|
|
|
5552
5699
|
}
|
|
5553
5700
|
}
|
|
5554
5701
|
const providerSpecs = {
|
|
5555
|
-
openai: { pkg: "@ai-sdk/openai", factory: "openai", defaultModel: "gpt-4o", displayName: "OpenAI" },
|
|
5556
|
-
anthropic: { pkg: "@ai-sdk/anthropic", factory: "anthropic", defaultModel: "claude-sonnet-4-20250514", displayName: "Anthropic" },
|
|
5557
|
-
google: { pkg: "@ai-sdk/google", factory: "google", defaultModel: "gemini-2.0-flash", displayName: "Google" }
|
|
5702
|
+
openai: { pkg: "@ai-sdk/openai", factory: "openai", createFactory: "createOpenAI", defaultModel: "gpt-4o", displayName: "OpenAI" },
|
|
5703
|
+
anthropic: { pkg: "@ai-sdk/anthropic", factory: "anthropic", createFactory: "createAnthropic", defaultModel: "claude-sonnet-4-20250514", displayName: "Anthropic" },
|
|
5704
|
+
google: { pkg: "@ai-sdk/google", factory: "google", createFactory: "createGoogleGenerativeAI", defaultModel: "gemini-2.0-flash", displayName: "Google" }
|
|
5558
5705
|
};
|
|
5559
5706
|
const spec = providerSpecs[provider];
|
|
5560
5707
|
if (!spec) return null;
|
|
@@ -5567,20 +5714,30 @@ var AIServicePlugin = class {
|
|
|
5567
5714
|
if (!apiKey) return null;
|
|
5568
5715
|
const envKey = provider === "openai" ? "OPENAI_API_KEY" : provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_GENERATIVE_AI_API_KEY";
|
|
5569
5716
|
process.env[envKey] = apiKey;
|
|
5717
|
+
const baseUrl = String(values[`${provider}_base_url`] ?? "").trim() || void 0;
|
|
5570
5718
|
try {
|
|
5571
5719
|
const mod = await import(
|
|
5572
5720
|
/* webpackIgnore: true */
|
|
5573
5721
|
spec.pkg
|
|
5574
5722
|
);
|
|
5575
|
-
|
|
5723
|
+
let factory = mod[spec.factory] ?? mod.default;
|
|
5724
|
+
if (baseUrl) {
|
|
5725
|
+
const createFn = mod[spec.createFactory];
|
|
5726
|
+
if (typeof createFn === "function") {
|
|
5727
|
+
factory = createFn({ apiKey, baseURL: baseUrl });
|
|
5728
|
+
} else {
|
|
5729
|
+
ctx.logger.warn(`[AI] ${spec.pkg} has no ${spec.createFactory}; baseURL override ignored.`);
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5576
5732
|
if (typeof factory !== "function") return null;
|
|
5577
5733
|
const modelId = String(values[`${provider}_model`] ?? "").trim() || spec.defaultModel;
|
|
5578
5734
|
const useChatApi = provider === "openai" && typeof factory.chat === "function";
|
|
5579
5735
|
const model = useChatApi ? factory.chat(modelId) : factory(modelId);
|
|
5580
5736
|
const apiSuffix = useChatApi ? " [chat-completions]" : "";
|
|
5737
|
+
const baseSuffix = baseUrl ? ` @ ${baseUrl}` : "";
|
|
5581
5738
|
return {
|
|
5582
5739
|
adapter: new VercelLLMAdapter({ model }),
|
|
5583
|
-
description: `${spec.displayName} (model: ${modelId})${apiSuffix}`
|
|
5740
|
+
description: `${spec.displayName} (model: ${modelId})${apiSuffix}${baseSuffix}`
|
|
5584
5741
|
};
|
|
5585
5742
|
} catch (err) {
|
|
5586
5743
|
ctx.logger.warn(
|
|
@@ -6109,7 +6266,14 @@ var AIServicePlugin = class {
|
|
|
6109
6266
|
for (const [k, v] of Object.entries(payload.values)) {
|
|
6110
6267
|
values[k] = v?.value;
|
|
6111
6268
|
}
|
|
6112
|
-
const
|
|
6269
|
+
const providerForTitles = String(values.provider ?? "memory");
|
|
6270
|
+
const titleEnabled = providerForTitles !== "memory" && values.title_generation_enabled !== false;
|
|
6271
|
+
const titleMaxLen = typeof values.title_max_length === "number" ? values.title_max_length : 16;
|
|
6272
|
+
this.service.setTitleGenerationConfig({
|
|
6273
|
+
enabled: titleEnabled,
|
|
6274
|
+
maxLength: titleMaxLen
|
|
6275
|
+
});
|
|
6276
|
+
const provider = providerForTitles;
|
|
6113
6277
|
if (provider === "memory") return;
|
|
6114
6278
|
const built = await this.buildAdapterFromValues(ctx, values);
|
|
6115
6279
|
if (!built) {
|