All files / src/app/extensions/copilot-embedded/utils/copilot-embedded-chat copilot-embedded-chat.service.ts

93.54% Statements 29/31
75% Branches 9/12
83.33% Functions 10/12
93.33% Lines 28/30

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 1115x 5x 5x   5x 5x 5x 5x 5x   5x 5x           5x             5x   4x 4x 4x 4x 4x 4x 4x                 2x       2x                             2x 2x                                   2x                 2x     2x                       2x 1x            
import { Injectable } from '@angular/core';
import { Observable, combineLatest, of } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
 
import { AppFacade } from 'ish-core/facades/app.facade';
import { CheckoutFacade } from 'ish-core/facades/checkout.facade';
import { FeatureToggleService } from 'ish-core/feature-toggle.module';
import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service';
import { whenTruthy } from 'ish-core/utils/operators';
 
import { OrderTemplatesFacade } from '../../../order-templates/facades/order-templates.facade';
import { CopilotEmbeddedFacade } from '../../facades/copilot-embedded.facade';
import { CopilotEmbeddedAnswer } from '../../models/copilot-embedded-answer/copilot-embedded-answer.model';
import {
  CopilotEmbeddedRequest,
  CopilotEmbeddedRequestOptions,
} from '../../models/copilot-embedded-request/copilot-embedded-request.model';
import { CopilotEmbeddedService } from '../../services/copilot-embedded/copilot-embedded.service';
 
/**
 * Sends chat messages to the Copilot Embedded chatflow and enriches every request with the current PWA context
 * (chatflow variables, basket, order templates and shown products), see the Copilot Embedded guide.
 */
@Injectable({ providedIn: 'root' })
export class CopilotEmbeddedChatService {
  constructor(
    private copilotEmbeddedFacade: CopilotEmbeddedFacade,
    private appFacade: AppFacade,
    private apiTokenService: ApiTokenService,
    private checkoutFacade: CheckoutFacade,
    private orderTemplatesFacade: OrderTemplatesFacade,
    private featureToggleService: FeatureToggleService,
    private copilotEmbeddedService: CopilotEmbeddedService
  ) {}
 
  /**
   * Whether to stream the responses: honors the `streaming` configuration, otherwise probes the chatflow.
   *
   * @returns           Emits once whether the answers are streamed.
   */
  isStreamingAvailable$(): Observable<boolean> {
    return this.copilotEmbeddedFacade.configuration$.pipe(
      whenTruthy(),
      take(1),
      switchMap(config =>
        typeof config.streaming === 'boolean'
          ? of(config.streaming)
          : this.copilotEmbeddedService.isStreamingAvailable$()
      )
    );
  }
 
  /**
   * Sends a message to the Copilot Embedded and returns the complete answer.
   *
   * @param question    The user's question; may be empty if an image is attached via `options.uploads`.
   * @param options     Session, chat instance, attachments, shown products and additional chatflow variables of the request.
   * @returns           Emits the complete answer once; errors if the request fails.
   */
  sendMessage(question: string, options?: CopilotEmbeddedRequestOptions): Observable<CopilotEmbeddedAnswer> {
    return this.chatRequest$(question, options).pipe(
      switchMap(request => this.copilotEmbeddedService.sendMessage(request))
    );
  }
 
  /**
   * Sends a message to the Copilot Embedded and emits the answer received so far as it is generated.
   *
   * @param question    The user's question; may be empty if an image is attached via `options.uploads`.
   * @param options     Session, chat instance, attachments, shown products and additional chatflow variables of the request.
   * @returns           Emits the answer received so far on every change and completes with the stream; errors if the request fails.
   */
  streamMessage(question: string, options?: CopilotEmbeddedRequestOptions): Observable<CopilotEmbeddedAnswer> {
    return this.chatRequest$(question, options).pipe(
      switchMap(request => this.copilotEmbeddedService.streamMessage(request))
    );
  }
 
  private chatRequest$(question: string, options: CopilotEmbeddedRequestOptions): Observable<CopilotEmbeddedRequest> {
    return combineLatest([
      this.copilotEmbeddedFacade.configuration$.pipe(whenTruthy()),
      this.appFacade.getRestEndpointWithContext$,
      this.appFacade.currentLocale$,
      this.apiTokenService.apiToken$,
      this.checkoutFacade.basketLineItems$,
      // order templates are a lazy, feature-gated store; only read them when the feature is enabled
      this.featureToggleService
        .enabled$('orderTemplates')
        .pipe(switchMap(enabled => (enabled ? this.orderTemplatesFacade.orderTemplates$ : of([])))),
    ]).pipe(
      take(1),
      map(([config, restEndpoint, currentLocale, userToken, lineItems, orderTemplates]): CopilotEmbeddedRequest => ({
        question,
        sessionId: options?.sessionId,
        chatId: options?.chatId,
        uploads: options?.uploads,
        vars: {
          restEndpoint,
          currentLocale,
          ...(userToken ? { user_token: userToken } : {}),
          ...config.chatflowConfig?.vars,
          ...options?.vars,
        },
        basket: (lineItems ?? []).map(li => ({ sku: li.productSKU, quantity: li.quantity?.value })),
        orderTemplates: (orderTemplates ?? []).map(t => ({ id: t.id, title: t.title })),
        shownProducts: options?.shownProducts ?? [],
      }))
    );
  }
}