在 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: `
Chat Genie
`,
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;
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`。