ChatGPT Chatbot
Last updated
Last updated
Ne mutăm în terminal și instalăm pachetul OpenAI.
npm install openai
Accesăm link-ul https://platform.openai.com/login și ne facem un cont pentru a putea genera o cheie secretă prin care o să avem dreptul să utilizăm ChatGPT.
După autentificare, selectăm varianta API.
Urmează să mergem la secțiunea dedicată cheilor API.
Apăsăm pe butonul de creat o nouă cheie secretă, îi dăm o denumire, îi alocăm toate permisiunile și apăsăm să o creăm.
Avem grijă să copiem cheia generată și să o mutăm în cadrul fișierului cu chei de mediu.
Este singura dată când putem să vedem cheia.
Fișierul nostru de .env ar trebui să arate ceva de genul acesta:
NODE_ENV = development
NEXT_ATLAS_URI = mongodb+srv://USERNAME:PASSWORD@cluster0.tt3lolq.mongodb.net/
NEXT_ATLAS_DATABASE = CloudComputing
OPENAI_API_KEY = KEY
Pentru activarea API key-ului, este nevoie să adăugăm o metodă de plată.
Nicio sumă de bani nu va fi retrasă de pe cont pentru verificare.
Activarea API KEY-ului presupune adăugarea unui credit de minim de 5 euro pentru a putea folosi cheia.
Mergem în cadrul folderului lib și adăugăm un nou fișier, numit openai.js.
Acest fișier are rolul de a ne genera o instanță de openai, prin intermediul căreia o să putem face cereri.
Dacă nu ați repornit aplicația după ce ați adăugat noua cheia de mediu, o să trebuiască să îl reporniți pentru a prelua și noua cheie pe care tocmai am adăugat-o.
import OpenAI from 'openai';
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error("Please define OPENAI_API_KEY in your environment variables");
}
const openai = new OpenAI(apiKey);
export default openai;
Ne mutăm în folderul api și adăugăm un nou fișier numit answer.js.
// /pages/api/answer.js
import openai from "@/lib/openai";
import { sendBadRequest, sendMethodNotAllowed, sendOk } from "@/utils/apiMethods";
const SYSTEM_PROMPTS = {
SIMPLE_ASSISTANT: {
MESSAGE: {
'role': 'system',
'content': 'You are a simple assistant. You respond with simple sentences.',
},
TEMPERATURE: 1,
MAX_TOKENS: 50,
TYPE: 'simple_assistant',
},
USER: {
MESSAGE: {
'role': 'system',
'content': 'You are a user. You respond with normal sentences.',
},
TEMPERATURE: 1,
MAX_TOKENS: 100,
TYPE: 'user',
},
};
const ERRORS = {
DATABASE_ERROR: {
type: 'database_error',
message: 'Error while processing the request.',
},
WRONG_CONVERSATION_TYPE: {
type: 'wrong_conversation_type',
message: 'The conversation type is not known.',
},
OPEN_AI_ERROR: {
type: 'open_ai_error',
message: 'Error while processing the request.',
},
};
const chatCompletion = async (messagesArray, max_tokens, temperature) => {
const rawResponse = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: messagesArray,
max_tokens: max_tokens,
temperature: temperature,
});
return rawResponse?.choices[0];
}
const converseChat = async (res, inputChat, useCase) => {
try{
const MAX_MEMORY = 3;
let userMessagesArray = [];
if (inputChat.length > MAX_MEMORY) {
userMessagesArray = inputChat.slice(-MAX_MEMORY);
}
else {
userMessagesArray = inputChat;
}
const messagesArray = [
useCase.MESSAGE,
...userMessagesArray
];
const response = await chatCompletion(messagesArray, useCase.MAX_TOKENS, useCase.TEMPERATURE);
return sendOk(res, response);
}
catch(error) {
console.error(error);
return sendBadRequest(res, ERRORS.OPEN_AI_ERROR.type, ERRORS.OPEN_AI_ERROR.message);
}
}
const converse = (res, messages, type) => {
switch (type) {
case SYSTEM_PROMPTS.SIMPLE_ASSISTANT.TYPE:
return converseChat(res, messages, SYSTEM_PROMPTS.SIMPLE_ASSISTANT);
case SYSTEM_PROMPTS.USER.TYPE:
return converseChat(res, messages, SYSTEM_PROMPTS.USER);
default:
return sendBadRequest(res, 'wrong_conversation_type');
}
}
export default async function handler(req, res) {
const isAllowedMethod = req.method === 'POST';
const { messages, type } = req.body;
if (!isAllowedMethod) {
return sendMethodNotAllowed()
}
else if (!messages) {
return sendBadRequest(res, 'Missing input');
}
else if(!type) {
return sendBadRequest(res, 'wrong_conversation_type');
}
try{
return converse(res, messages, type);
}
catch(error) {
console.error(error);
}
}
Cum functionează codul din acest template de cod:
În primă instanță, avem declarat un obiect SYSTEM_PROMPTS, care are scopul de a defini cele 2 roluri din cadrul chat-ului, și anume asistentul virtual și persoana care i se adresează.
Ulterior, fiecare rol primește mai multe setări, printre care TEMPERATURE (cu cât temperatura este mai mare, cu atât este mai probabil ca modelul să genereze răspunsuri neașteptate sau creative) și MAX_TOKENS (Cu cât este mai mare acest număr, cu atât răspunsul poate fi mai lung și mai detaliat).
Avem un obiect ERRORS care definește tipurile de erori pe care le putem întâmpina.
Definim o funcție async numită chatCompletion care are rolul de a întreba asistentul, cu versiunea gpt-3.5-turbo și de a returna răspunsul. Alte modele în afară de gpt-3.5-turbo poate avea ca și consecințte costuri suplimentare. Lista cu toate tipurile de modele: https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo
Funcția converseChat are rolul de a iniția o conversație între utilizator și un asistent AI folosind modelul GPT-3.5. Funcția începe prin a verifica dacă numărul de mesaje trimise de utilizator depășește un anumit prag și, în funcție de acest lucru, selectează ultimele MAX_MEMORY
mesaje sau toate mesajele dacă numărul acestora este mai mic decât pragul. Apoi, construiește un array de mesaje care include mesajele predefinite pentru tipul de conversație și mesajele utilizatorului.
Funcțiaconverse
este funcția principală care gestionează conversația între utilizator și asistentul AI. Ea verifică tipul de conversație specificat și apoi apelează funcția converseChat
pentru a începe conversația corespunzătoare.
Funcțiahandler
este funcția de bază care servește drept punct de intrare pentru gestionarea cererilor HTTP către server. Această funcție primește un obiect req
care reprezintă cererea HTTP primită de server și un obiect res
care reprezintă răspunsul pe care serverul îl va trimite înapoi către client.
Ne mutăm în folderul pages și o să adăugăm o noua pagină, numită chat.jsx
// /pages/chat.jsx
import ChatComponent from "@/js/components/ChatComponent";
const ChatPage = () => {
return (
<div>
<ChatComponent />
</div>
)
}
export default ChatPage;
Ne mutăm în folderul components și creăm o noua componentă, numită ChatComponent.jsx.
// /components/ChatComponent.jsx
import React, {useState} from 'react';
import MessageBox from "@/js/components/MessageBox";
function ChatComponent(props) {
const [chatMessages, setChatMessages] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [userInput, setUserInput] = useState('');
const filterChatHistory = chatHistory => {
let filteredChatHistory = [];
for( let i = 0; i < chatHistory.length; i++ ){
const currMessage = chatHistory[i];
const nextMessage = chatHistory[i+1];
if( i === chatHistory.length - 1 || (currMessage.type !== 'error' && nextMessage?.type !== 'error' && currMessage.role !== nextMessage?.role) ){
filteredChatHistory.push(currMessage);
}
}
return filteredChatHistory;
}
const buildResponseMessageObject = response => {
let responseMessageObject;
responseMessageObject = response.data.message;
return responseMessageObject;
}
const handleKeyDown = async (e) => {
if(e.key === 'Enter'){
if(userInput.length === 0) {
return;
}
const currentMessageObject = {
role: 'user',
content: userInput,
}
setChatMessages(prevChatMessages => [...prevChatMessages, currentMessageObject]);
const currentChatHistory = [...chatMessages, currentMessageObject];
const filteredChatHistory = filterChatHistory(currentChatHistory);
try{
setIsLoading(true);
const response = await fetch('/api/answer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
messages: filteredChatHistory,
type: 'user',
}),
});
const json = await response.json();
setUserInput('');
setIsLoading(false);
const responseMessageObject = buildResponseMessageObject(json);
setChatMessages(prevChatMessages => [...prevChatMessages, responseMessageObject]);
}
catch(error){
console.log(error);
}
}
}
return (
<div className={"w-full max-w-[1500px] mx-auto my-10"}>
<div className={"border border-b-0 rounded-lg border-gray-300'"}>
<div className={'border-b text-center px-[20px] py-[10px]'}>
<span className={'text-md font-bold text-gray-900'}>
This a chat component that looks like Yahoo!
</span>
</div>
<MessageBox chatMessages={chatMessages}/>
</div>
<input
id={'chat-input'}
type={'text'}
value={userInput}
onChange={e => setUserInput(e.target.value)}
disabled={isLoading}
className="bg-gray-50 border border-gray-300 border-x-0 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-4"
placeholder="Type something..."
onKeyDown={handleKeyDown}
/>
</div>
);
}
export default ChatComponent;
La începutul componentei, observăm că avem 3 state-uri:
chatMessages -> salvează toată conversația între utilizator și chatbot
isLoading -> folosit pentru a da disable la input cât timp request-ul este încă în lucru
userInput -> textul curent pe care îl scrie utilizatorul
Principalele funcții:
FuncțiafilterChatHistory
este responsabilă pentru filtrarea istoricului de conversație pentru a elimina mesajele redundante și erorile din istoricul chatului. Această funcție primește ca parametru istoricul de chat și returnează un nou array care conține doar mesajele relevante pentru afișare.
FuncțiabuildResponseMessageObject
este responsabilă pentru construirea unui obiect de mesaj din răspunsul primit de la server în urma unei solicitări către API-ul de procesare a mesajelor. Această funcție primește ca parametru răspunsul JSON primit de la server și returnează un obiect de mesaj care poate fi adăugat la istoricul de chat și afișat utilizatorului.
FuncțiahandleKeyDown
este un handler de eveniment care este activat atunci când utilizatorul apasă o tastă în câmpul de introducere a textului. Ulterior, face requestul de interogare a asistentului virtual și afișarea răspunsului.
În final, mesajele sunt transmise către o altă componentă, numită MessageBox, care are rolul să afișeze mesajele.
Tot în folderul components, adăugăm fișierul MessageBox.jsx.
// /components/MessageBox.jsx
import React, {useEffect, useRef} from 'react';
function MessageBox(props) {
const messagesEndRef = useRef(null);
const { chatMessages, } = props;
const scrollToBottom = () => {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth', });
}
useEffect(() => {
scrollToBottom()
}, [chatMessages]);
return (
<div className={'h-[400px] overflow-auto px-[20px]'}>
<ul className={'divide-y divide-gray-200'}>
<li className={ 'py-3' }>
<span className={ `block text-green-800 font-bold` }>
ChatGPT:
</span>
<span className={ 'block' }>
Hi, I am a ChatBot. How can I help you?
</span>
</li>
{
chatMessages.map((message, index) => {
return (
<li className={'py-3'} key={index}>
<span className={ `block ${message.role === 'assistant' ? 'text-green-800' : 'text-blue-800'} font-bold` }>
{
message.role === 'assistant' ? 'ChatGPT:' : 'You:'
}
</span>
<span className={ 'block' }>
{
message.content
}
</span>
</li>
)
})
}
</ul>
<div ref={ messagesEndRef } />
</div>
);
}
export default MessageBox;