在 Angular App 中集成 OpenAI
概述
生成式人工智能如今风靡一时。似乎我们一天都离不开人工智能,尤其是在科技行业。OpenAI 是其中的一个重要原因。OpenAI 提供了一个 API 来利用他们训练过的模型。他们提供的最受欢迎的访问方式之一是访问他们的 LLM(大型语言模型)。
我们正在做什么?
在本文中,我们将展示如何利用 OpenAI 创建一个聊天机器人,您可以提出最多 3 个问题,它会为每个问题生成一个答案。我们称该应用程序为 Chat Genie。
该代码位于 Github 上。
我们没有做什么?
此应用将使用 Angular 和 NgRx。我们将展示并解释代码,但不会深入解释 NgRx。
OpenAI 力学
OpenAI 允许您调用其文本生成服务的 API。文档位于此处。使用此 API,您可以将问题串联起来以继续构建上下文。在我们的应用程序中,我们最多允许将 3 个问题串联在一起。
让我们开始吧
import { NgClass } from '@angular/common'; import { Component, computed, ElementRef, inject, ViewChild } from '@angular/core'; import { Store } from '@ngrx/store'; import { v4 } from 'uuid'; import { OpenAIHttpPostRequest } from './model/message.interface'; import { addMessage, resetMessages } from './ngrx/actions/messages.action'; import { selectMessages, selectRequest } from './ngrx/selector/messages.selector'; @Component({ selector: 'app-root', standalone: true, imports: [NgClass], template: ``, styles: ` .full-height { height: 100vh; } header { height: 3rem; background-color: #2563eb; color: white; display: flex; justify-content: center; align-items: center; font-size: 2rem; } button { padding: .5rem; text-align: center; margin-left: .5rem; border-radius: 8px; color: white; border: none; &:disabled { opacity: 0.5; } &.primary { background-color: #2563eb; } &.default { background-color: #a8a29e; color: #111827; } } .messages-container { height: calc(100vh - 9rem); // header + footer + margin-top overflow-y: auto; display: flex; flex-direction: column; margin: 1rem 1rem 0 1rem; .messages { border-radius: 8px; padding: 1.5rem; width: 20rem; margin-bottom: 1rem; text-align: center; &.user { align-self: flex-start; background-color: #fcd34d; } &.openAI { align-self: flex-end; background-color: #bbf7d0; } } } footer { position: sticky; bottom: 0; height: 5rem; justify-self: center; .action-container { display: inline-flex; height: 50%; justify-content: center; input { width: 50vw; border-radius: 8px; } } } ` }) export class AppComponent { @ViewChild('messageInput') messageInput!: ElementRef Chat Genie ; private _store = inject(Store); messagesS = this._store.selectSignal(selectMessages); isSendDisabled = computed(() => { const messages = this._store.selectSignal(selectMessages)(); return messages.some(message => message.isProcessing) || messages.length > 6; }); addMessage(message: string) { const storeRequest = this._store.selectSignal(selectRequest)(); let messages: { role: string; content: string; }[] = []; if (!storeRequest) { messages = [{ role: 'system', content: 'You are a helpful assistant.' }, { role: 'user', content: message }] } else { messages = [...storeRequest.messages, { role: 'user', content: message }]; } const requestBody: OpenAIHttpPostRequest = { appId: v4(), model: 'gpt-3.5-turbo', messages, temperature: 0.7 }; this._store.dispatch(addMessage(requestBody)); this.messageInput.nativeElement.value = ''; } resetQuestions(): void { this._store.dispatch(resetMessages()); } }
这是显示消息的组件。当用户点击“发送”按钮时,将分派一个操作来启动请求。
import { Injectable, inject } from '@angular/core'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { OpenAiHttpService } from '../../services/open-ai-http.service'; import { addMessage, addMessageSuccess, openAIError } from '../actions/messages.action'; import { catchError, filter, map, switchMap, withLatestFrom } from 'rxjs'; import { Store } from '@ngrx/store'; import { selectMessages } from '../selector/messages.selector'; import { OpenAIResponse } from '../../model/message.interface'; @Injectable() export class MessagesEffects { private _actions$ = inject(Actions); private _openAiHttpService = inject(OpenAiHttpService); private _store = inject(Store); addMessage$ = createEffect( () => this._actions$.pipe( ofType(addMessage), withLatestFrom(this._store.select(selectMessages)), filter(([action, messages]) => { return messages.length <= 6; }), switchMap(([action]) => this._openAiHttpService.getOpenAiResponse(action.requestBody).pipe( map(response => addMessageSuccess({ response })), catchError(error => ([openAIError({ error })])) )) ) ); addMessagesLimit$ = createEffect( () => this._actions$.pipe( ofType(addMessage), withLatestFrom(this._store.select(selectMessages)), filter(([action, messages]) => messages.length > 6), map(([action]) => { const updatedMessage: OpenAIResponse = { model: action.requestBody.model, appId: action.requestBody.appId, choices: [ { index: 0, message: { content: 'The Great Chat Genien only answers 3 questions!', role: 'openAI' } } ] }; return addMessageSuccess({ response: updatedMessage }); }) ) ); }
此文件显示了 2 个效果。一个是调用向 OpenAI 发出 HTTP 请求的服务。仅当存储中的消息数为 6 或更少时才会调用此效果。之所以为 6,是因为我们允许 3 个问题,每个问题由 2 条消息组成,一条是用户问题,另一条是来自 OpenAI 的回复。第二个效果添加了一条新消息,让用户知道已达到限制,而不是向 OpenAI 发出此 HTTP 调用。
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable, inject } from '@angular/core'; import { OpenAIHttpPostRequest, OpenAIResponse } from '../model/message.interface'; import { Observable, catchError, map, throwError } from 'rxjs'; import { OPEN_AI_API_KEY } from '../secrets'; @Injectable({ providedIn: 'root' }) export class OpenAiHttpService { private _httpClient = inject(HttpClient); private readonly _OPEN_AI_API_URL = 'https://api.openai.com/v1/chat/completions'; private readonly _OPEN_AI_API_KEY = OPEN_AI_API_KEY; getOpenAiResponse(reqBody: OpenAIHttpPostRequest): Observable{ let headers = new HttpHeaders(); headers = headers.append('Authorization', `Bearer ${this._OPEN_AI_API_KEY}`); headers = headers.append('Content-Type', 'application/json'); const clonedBody = {...reqBody}; delete clonedBody.appId; return this._httpClient.post (this._OPEN_AI_API_URL, clonedBody, {headers}).pipe( map(response => ({...response, appId: reqBody.appId!})), catchError((error: any) => { console.error('Error in OpenAiHttpService:', error); return throwError(() => error); }) ) } }
这里需要注意的一点是,我们从请求主体中删除了属性“appId”。此属性在 Reducer 中用于将 Store 中待处理的请求映射到 API 请求成功后的响应。
export interface OpenAIHttpPostRequest { appId?: string; model: string; messages: { role: string; content: string; }[]; temperature: number; } export interface OpenAIResponse { appId?: string; id?: string; object?: string; created?: Date; model: string; messages?: {role: string; content: string}[]; choices: { index: number; message: { content: string; role: string; }, logprobs?: string; }[]; usage?: OpenAIUsage; } export interface OpenAIUsage { prompt_tokens: number; completion_tokens: number; total_tokens: number; }
这是来自 OpenAI 合约的请求和响应的接口。
import { createReducer, on } from '@ngrx/store'; import { OpenAIHttpPostRequest, OpenAIResponse } from '../../model/message.interface'; import { addMessage, addMessageSuccess, resetMessages } from '../actions/messages.action'; export interface MessageState { request: OpenAIHttpPostRequest | undefined; messages: {appId?: string, role: string, content: string, isProcessing: boolean}[]; } export const initialState: MessageState = { request: undefined, messages: [] }; export const messageReducer = createReducer( initialState, on( addMessage, (state, { requestBody }) => { const lastQuestionIndex = requestBody.messages.length - 1; const newMessage: {role: string, content: string, isProcessing: boolean} = { role: requestBody.messages[lastQuestionIndex].role, content: requestBody.messages[lastQuestionIndex].content, isProcessing: false }; return { ...state, messages: [...state.messages, newMessage, {appId: requestBody.appId!, role: 'openAI', content: '...', isProcessing: true}], request: requestBody } } ), on( addMessageSuccess, (state, { response }) => { const foundQuestionIndex = state.messages.findIndex((message) => message.appId === response.appId); const newMessage: {appId: string, role: string, content: string, isProcessing: boolean} = { appId: response.appId!, role: 'openAI', content: response.choices[0].message.content, isProcessing: false }; const updatedMessages = structuredClone(state.messages); updatedMessages[foundQuestionIndex] = newMessage; return { ...state, messages: updatedMessages }; } ), on( resetMessages, (state) => { return initialState; } ) );
这就是更新 `app.component.ts` 监听的存储的内容。当 `addMessage` 操作被分派时,我们会更新消息值以将新请求附加到“用户”角色项,并使用 `...` 值设置临时的“openAI”角色。成功消息后,我们将 `...` 值切换为来自 OpenAI 的响应。
就这些了
我希望这篇文章对您有所帮助。所有代码都发布在我的 Github 上。唯一需要做的就是创建 `src/app/secrets.ts` 以包含您自己的 `OPEN_AI_API_KEY`。