@khirby/plugin-ai-compose 1.2.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/ai-compose-crypto.ts +1 -1
- package/src/ai-compose-generate.controller.ts +1 -1
- package/src/ai-compose-llm.service.ts +1 -1
- package/src/ai-compose-settings.controller.ts +1 -1
- package/src/ai-compose-settings.service.ts +1 -1
- package/src/ai-compose-suggest.controller.ts +1 -1
- package/src/ai-compose-suggest.service.ts +13 -7
- package/src/ai-compose.module.ts +1 -1
- package/src/ai-compose.spec.ts +23 -16
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khirby/plugin-ai-compose",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Khirby — AI-powered reply draft suggestions (BYOK, OpenAI-compatible)",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"khirby-plugin"
|
|
8
8
|
],
|
|
9
9
|
"peerDependencies": {
|
|
10
|
-
"@khirby/plugin-host": "^1.
|
|
10
|
+
"@khirby/plugin-host": "^1.2.0",
|
|
11
11
|
"@khirby/plugin-sdk": "^1.0.0",
|
|
12
12
|
"@nestjs/common": "*",
|
|
13
|
+
"@nestjs/core": "*",
|
|
13
14
|
"@nestjs/swagger": "*",
|
|
14
15
|
"drizzle-orm": "*"
|
|
15
16
|
},
|
package/src/ai-compose-crypto.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
RequireAnyPermission,
|
|
7
7
|
RequirePluginEnabled,
|
|
8
8
|
PluginEnabledGuard,
|
|
9
|
-
} from '
|
|
9
|
+
} from '@khirby/plugin-host';
|
|
10
10
|
import { AiComposeSuggestService, type NewsletterContentType } from './ai-compose-suggest.service';
|
|
11
11
|
import { AI_COMPOSE_PLUGIN_NAME } from './ai-compose-settings.service';
|
|
12
12
|
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
AppException,
|
|
4
4
|
isReasoningEffort,
|
|
5
5
|
type AiComposeLlmLike,
|
|
6
|
-
} from '
|
|
6
|
+
} from '@khirby/plugin-host';
|
|
7
7
|
import { AiComposeSettingsService } from './ai-compose-settings.service';
|
|
8
8
|
import { AiComposeSuggestService } from './ai-compose-suggest.service';
|
|
9
9
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
RequirePermission,
|
|
7
7
|
RequirePluginEnabled,
|
|
8
8
|
PluginEnabledGuard,
|
|
9
|
-
} from '
|
|
9
|
+
} from '@khirby/plugin-host';
|
|
10
10
|
import { AiComposeSettingsService } from './ai-compose-settings.service';
|
|
11
11
|
import { AiComposeSuggestService } from './ai-compose-suggest.service';
|
|
12
12
|
import { AI_COMPOSE_PLUGIN_NAME } from './ai-compose-settings.service';
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
AppException,
|
|
9
9
|
isReasoningEffort,
|
|
10
10
|
type ReasoningEffort,
|
|
11
|
-
} from '
|
|
11
|
+
} from '@khirby/plugin-host';
|
|
12
12
|
import { aiComposeSettings } from './schema';
|
|
13
13
|
import { encrypt, decrypt, isAiComposeSecretsKeyConfigured } from './ai-compose-crypto';
|
|
14
14
|
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
RequireAnyPermission,
|
|
7
7
|
RequirePluginEnabled,
|
|
8
8
|
PluginEnabledGuard,
|
|
9
|
-
} from '
|
|
9
|
+
} from '@khirby/plugin-host';
|
|
10
10
|
import { AiComposeSuggestService } from './ai-compose-suggest.service';
|
|
11
11
|
import { AI_COMPOSE_PLUGIN_NAME } from './ai-compose-settings.service';
|
|
12
12
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Injectable, Inject, Logger
|
|
1
|
+
import { Injectable, Inject, Logger } from '@nestjs/common';
|
|
2
|
+
import { ModuleRef } from '@nestjs/core';
|
|
2
3
|
import {
|
|
3
4
|
LEADS_SERVICE,
|
|
4
5
|
MAIL_THREAD_SERVICE,
|
|
@@ -9,7 +10,8 @@ import {
|
|
|
9
10
|
applyReasoningEffort,
|
|
10
11
|
isReasoningEffort,
|
|
11
12
|
parseModelReasoningSupport,
|
|
12
|
-
|
|
13
|
+
resolveLoadedProvider,
|
|
14
|
+
} from '@khirby/plugin-host';
|
|
13
15
|
import { AiComposeSettingsService } from './ai-compose-settings.service';
|
|
14
16
|
|
|
15
17
|
const CHAR_BUDGET = 14_000;
|
|
@@ -44,11 +46,14 @@ export class AiComposeSuggestService {
|
|
|
44
46
|
private readonly settings: AiComposeSettingsService,
|
|
45
47
|
@Inject(MAIL_THREAD_SERVICE) private readonly mailThreads: MailThreadServiceLike,
|
|
46
48
|
@Inject(LEADS_SERVICE) private readonly leads: LeadsServiceLike,
|
|
47
|
-
|
|
48
|
-
@Inject(KNOWLEDGE_CONTEXT)
|
|
49
|
-
private readonly knowledge: KnowledgeContextLike | null,
|
|
49
|
+
private readonly moduleRef: ModuleRef,
|
|
50
50
|
) {}
|
|
51
51
|
|
|
52
|
+
/** Volume Pokelo may bind after this service is constructed (ADR-0048 / ADR-0050). */
|
|
53
|
+
private knowledge(): KnowledgeContextLike | null {
|
|
54
|
+
return resolveLoadedProvider<KnowledgeContextLike>(this.moduleRef, KNOWLEDGE_CONTEXT);
|
|
55
|
+
}
|
|
56
|
+
|
|
52
57
|
async availability(): Promise<{ available: boolean; defaultModel: string | null }> {
|
|
53
58
|
const defaultModel = await this.settings.getDefaultModel();
|
|
54
59
|
try {
|
|
@@ -342,8 +347,9 @@ export class AiComposeSuggestService {
|
|
|
342
347
|
const { apiKey, baseUrl } = await this.settings.getDecryptedApiKey();
|
|
343
348
|
|
|
344
349
|
const knowledgeQuery = (input.ragQuery ?? input.userContent).slice(0, 800);
|
|
345
|
-
const
|
|
346
|
-
|
|
350
|
+
const knowledge = this.knowledge();
|
|
351
|
+
const knowledgeSnippets = knowledge
|
|
352
|
+
? await knowledge.fetchContext(knowledgeQuery).catch(() => '')
|
|
347
353
|
: '';
|
|
348
354
|
|
|
349
355
|
const systemContent = [input.systemContent, knowledgeSnippets].filter(Boolean).join('\n\n');
|
package/src/ai-compose.module.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Global, Module } from '@nestjs/common';
|
|
2
|
-
import { AI_COMPOSE_LLM } from '
|
|
2
|
+
import { AI_COMPOSE_LLM } from '@khirby/plugin-host';
|
|
3
3
|
import { AiComposeSettingsService } from './ai-compose-settings.service';
|
|
4
4
|
import { AiComposeSuggestService } from './ai-compose-suggest.service';
|
|
5
5
|
import { AiComposeLlmService } from './ai-compose-llm.service';
|
package/src/ai-compose.spec.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
AiComposeSuggestService,
|
|
6
6
|
stripCodeFences,
|
|
7
7
|
} from './ai-compose-suggest.service';
|
|
8
|
-
import { AppException } from '
|
|
8
|
+
import { AppException } from '@khirby/plugin-host';
|
|
9
9
|
|
|
10
10
|
// ──────────────────────────────────────────────────────────────────────────────
|
|
11
11
|
// Crypto round-trip
|
|
@@ -255,6 +255,13 @@ const MOCK_THREAD = {
|
|
|
255
255
|
],
|
|
256
256
|
};
|
|
257
257
|
|
|
258
|
+
|
|
259
|
+
function mockModuleRef(knowledge: unknown = null) {
|
|
260
|
+
return {
|
|
261
|
+
get: jest.fn().mockReturnValue(knowledge),
|
|
262
|
+
} as any;
|
|
263
|
+
}
|
|
264
|
+
|
|
258
265
|
describe('AiComposeSuggestService', () => {
|
|
259
266
|
const mockMailThreads = {
|
|
260
267
|
getThread: jest.fn().mockResolvedValue(MOCK_THREAD),
|
|
@@ -302,7 +309,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
302
309
|
settings as any,
|
|
303
310
|
mockMailThreads as any,
|
|
304
311
|
mockLeads as any,
|
|
305
|
-
|
|
312
|
+
mockModuleRef(),
|
|
306
313
|
);
|
|
307
314
|
|
|
308
315
|
const result = await service.suggest({ threadId: 'thread-1', leadId: 'l-1' });
|
|
@@ -333,7 +340,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
333
340
|
settings as any,
|
|
334
341
|
mockMailThreads as any,
|
|
335
342
|
mockLeads as any,
|
|
336
|
-
|
|
343
|
+
mockModuleRef(),
|
|
337
344
|
);
|
|
338
345
|
await service.suggest({ threadId: 'thread-1' });
|
|
339
346
|
const body = chatBodyFromFetch();
|
|
@@ -356,7 +363,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
356
363
|
settings as any,
|
|
357
364
|
mockMailThreads as any,
|
|
358
365
|
mockLeads as any,
|
|
359
|
-
|
|
366
|
+
mockModuleRef(),
|
|
360
367
|
);
|
|
361
368
|
await service.suggest({ threadId: 'thread-1' });
|
|
362
369
|
const body = chatBodyFromFetch();
|
|
@@ -387,7 +394,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
387
394
|
settings as any,
|
|
388
395
|
mockMailThreads as any,
|
|
389
396
|
mockLeads as any,
|
|
390
|
-
|
|
397
|
+
mockModuleRef(),
|
|
391
398
|
);
|
|
392
399
|
await service.suggest({ threadId: 'thread-1' });
|
|
393
400
|
expect(completions).toBe(2);
|
|
@@ -411,7 +418,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
411
418
|
makeSettingsService() as any,
|
|
412
419
|
mockMailThreads as any,
|
|
413
420
|
mockLeads as any,
|
|
414
|
-
|
|
421
|
+
mockModuleRef(),
|
|
415
422
|
);
|
|
416
423
|
await expect(
|
|
417
424
|
service.fetchModels('https://api.openai.com/v1', 'sk-test'),
|
|
@@ -428,7 +435,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
428
435
|
settings as any,
|
|
429
436
|
mockMailThreads as any,
|
|
430
437
|
mockLeads as any,
|
|
431
|
-
|
|
438
|
+
mockModuleRef(),
|
|
432
439
|
);
|
|
433
440
|
|
|
434
441
|
await expect(service.suggest({ threadId: 'thread-1', model: 'claude-3' })).rejects.toThrow();
|
|
@@ -445,7 +452,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
445
452
|
settings as any,
|
|
446
453
|
mockMailThreads as any,
|
|
447
454
|
mockLeads as any,
|
|
448
|
-
|
|
455
|
+
mockModuleRef(),
|
|
449
456
|
);
|
|
450
457
|
|
|
451
458
|
await expect(service.suggest({ threadId: 'thread-1' })).rejects.toThrow(
|
|
@@ -465,7 +472,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
465
472
|
settings as any,
|
|
466
473
|
mockMailThreads as any,
|
|
467
474
|
mockLeads as any,
|
|
468
|
-
|
|
475
|
+
mockModuleRef(),
|
|
469
476
|
);
|
|
470
477
|
|
|
471
478
|
await expect(service.suggest({ threadId: 'thread-1' })).rejects.toThrow();
|
|
@@ -482,7 +489,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
482
489
|
settings as any,
|
|
483
490
|
mockMailThreads as any,
|
|
484
491
|
mockLeads as any,
|
|
485
|
-
|
|
492
|
+
mockModuleRef(),
|
|
486
493
|
);
|
|
487
494
|
|
|
488
495
|
await service.suggest({ threadId: 'thread-1', instruction: 'Be formal' });
|
|
@@ -506,7 +513,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
506
513
|
settings as any,
|
|
507
514
|
mockMailThreads as any,
|
|
508
515
|
mockLeads as any,
|
|
509
|
-
|
|
516
|
+
mockModuleRef(),
|
|
510
517
|
);
|
|
511
518
|
|
|
512
519
|
const result = await service.suggest({ leadId: 'l-1' });
|
|
@@ -530,7 +537,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
530
537
|
settings as any,
|
|
531
538
|
mockMailThreads as any,
|
|
532
539
|
mockLeads as any,
|
|
533
|
-
|
|
540
|
+
mockModuleRef(),
|
|
534
541
|
);
|
|
535
542
|
|
|
536
543
|
await expect(service.suggest({})).rejects.toThrow('Either threadId or leadId is required');
|
|
@@ -550,7 +557,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
550
557
|
settings as any,
|
|
551
558
|
mockMailThreads as any,
|
|
552
559
|
mockLeads as any,
|
|
553
|
-
knowledge
|
|
560
|
+
mockModuleRef(knowledge),
|
|
554
561
|
);
|
|
555
562
|
|
|
556
563
|
await service.suggest({ threadId: 'thread-1', leadId: 'l-1', instruction: 'Be brief' });
|
|
@@ -574,7 +581,7 @@ describe('AiComposeSuggestService', () => {
|
|
|
574
581
|
settings as any,
|
|
575
582
|
mockMailThreads as any,
|
|
576
583
|
mockLeads as any,
|
|
577
|
-
|
|
584
|
+
mockModuleRef(),
|
|
578
585
|
);
|
|
579
586
|
|
|
580
587
|
await service.suggest({ threadId: 'thread-1' });
|
|
@@ -602,7 +609,7 @@ describe('AiComposeSuggestService.generateNewsletter', () => {
|
|
|
602
609
|
settings as any,
|
|
603
610
|
mockMailThreads as any,
|
|
604
611
|
mockLeads as any,
|
|
605
|
-
|
|
612
|
+
mockModuleRef(),
|
|
606
613
|
);
|
|
607
614
|
}
|
|
608
615
|
|
|
@@ -704,7 +711,7 @@ describe('AiComposeSuggestService.generateNewsletter', () => {
|
|
|
704
711
|
makeSettingsService() as any,
|
|
705
712
|
mockMailThreads as any,
|
|
706
713
|
mockLeads as any,
|
|
707
|
-
knowledge
|
|
714
|
+
mockModuleRef(knowledge),
|
|
708
715
|
);
|
|
709
716
|
|
|
710
717
|
await svc.generateNewsletter({
|