@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 CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@khirby/plugin-ai-compose",
3
- "version": "1.2.0",
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.0.0",
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
  },
@@ -2,4 +2,4 @@ export {
2
2
  decrypt,
3
3
  encrypt,
4
4
  isInstanceSecretsKeyConfigured as isAiComposeSecretsKeyConfigured,
5
- } from '../../../packages/plugin-host/src/instance-secrets';
5
+ } from '@khirby/plugin-host';
@@ -6,7 +6,7 @@ import {
6
6
  RequireAnyPermission,
7
7
  RequirePluginEnabled,
8
8
  PluginEnabledGuard,
9
- } from '../../../packages/plugin-host/src';
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 '../../../packages/plugin-host/src';
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 '../../../packages/plugin-host/src';
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 '../../../packages/plugin-host/src';
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 '../../../packages/plugin-host/src';
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, Optional } from '@nestjs/common';
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
- } from '../../../packages/plugin-host/src';
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
- @Optional()
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 knowledgeSnippets = this.knowledge
346
- ? await this.knowledge.fetchContext(knowledgeQuery).catch(() => '')
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');
@@ -1,5 +1,5 @@
1
1
  import { Global, Module } from '@nestjs/common';
2
- import { AI_COMPOSE_LLM } from '../../../packages/plugin-host/src';
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';
@@ -5,7 +5,7 @@ import {
5
5
  AiComposeSuggestService,
6
6
  stripCodeFences,
7
7
  } from './ai-compose-suggest.service';
8
- import { AppException } from '../../../packages/plugin-host/src';
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
- null,
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
- null,
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
- null,
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
- null,
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
- null,
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
- null,
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
- null,
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
- null,
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
- null,
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
- null,
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
- null,
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 as any,
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
- null,
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
- null,
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 as any,
714
+ mockModuleRef(knowledge),
708
715
  );
709
716
 
710
717
  await svc.generateNewsletter({