ساخت یک Chatbot Agent با Node.js

ساخت یک Chatbot Agent با Node.js

chatbot, function-call, node.js
preview

قابلیت فراخوانی تابع در مدل‌های GPT به برنامه شما اجازه می دهد توابع داخلی برنامه را بر اساس ورودی های کاربر فراخوانی کند. این به این معنی است که برنامه می تواند عملیات مختلفی از جمله، جستجو در وب، ارسال ایمیل، یا رزرو بلیط از طرف کاربران را انجام دهد، که این امر برنامه شما را قدرتمندتر از یک چت بات معمولی می کند.

در این پست، شما برنامه‌ای می سازید که از آخرین نسخه از OpenAI SDK Node.js استفاده می کند. این برنامه در مرورگر اجرا می شود،

اگر Node.js بر روی کامپیوتر شما نصب نیست٬ می‌توانید از طریق Scrimba کد برنامه را نوشته و اجرا کنید.

نحوه عملکرد برنامه #

این برنامه یک عامل یا Agent ساده است که به شما کمک می کند تا فعالیت های مورد علاقه‌تان را در اطراف خود پیدا کنید.

این برنامه دسترسی به دو تابع، getLocation() و getCurrentWeather() دارد، که به این معنی است که می تواند متوجه شود که شما کجا قرار دارید و هوا در حال حاضر چگونه است.

لازم به ذکر است که GPT هیچ کدی را برای شما اجرا نمی کند. فقط به برنامه شما می گوید که کدام توابع را باید در یک سناریو مشخص استفاده کند، و فراخوانی آنها را به برنامه واگذار می کند.

هنگامی که Agent مکان شما و هوا را می داند، از دانش داخلی GPT برای پیشنهاد فعالیت های مناسب محلی برای شما استفاده می کند.

برای اجرای کدهای زیر ابتدا باید یک کلید API را از طریق پنل کاربری گیلاس تولید کنید. برای این کار ابتدا یک حساب کاربری جدید بسازید یا اگر صاحب حساب کاربری هستید وارد پنل کاربری خود شوید. سپس، به صفحه کلید API بروید و با کلیک روی دکمه “ساخت کلید API” یک کلید جدید برای دسترسی به Gilas API بسازید.

نوشتن کد برنامه #

ابتدا یک کلاینت از OpenAI بسازید.

1import OpenAI from "openai";
2 
3const openai = new OpenAI({
4  apiKey: process.env.GILAS_API_KEY, // <کلید API خود را اینجا بسازید https://dashboard.gilas.io/apiKey>
5  baseUrl: "https://api.gilas.io/v1/",
6  dangerouslyAllowBrowser: true,
7});

اگر کد خود را در محیط مرورگر در Scrimba اجرا می کنید٬ مقدار متغیر dangerouslyAllowBrowser: true را تنظیم کنید.

ساخت دو تابع برای برنامه #

ابتدا یک تابع به نام getLocation می‌سازیم که یا استفاده از سرویس IP API محل یوزر را تشخیص می‌دهد.

1async function getLocation() {
2  const response = await fetch("https://ipapi.co/json/");
3  const locationData = await response.json();
4  return locationData;
5}

سرویس IP API مجموعه ای از داده ها در مورد مکان شما را برمی گرداند، از جمله عرض و طول جغرافیایی که ما آنها را به عنوان آرگومان ها در تابع دوم getCurrentWeather استفاده می کنیم. ما از Open Meteo API برای دریافت داده های هوای فعلی استفاده می کند.

1async function getCurrentWeather(latitude, longitude) {
2  const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&hourly=apparent_temperature`;
3  const response = await fetch(url);
4  const weatherData = await response.json();
5  return weatherData;
6}

معرفی توابع به مدل GPT #

برای اینکه مدل GPT هدف این توابع را بفهمد، ما باید آنها را برای مدل توصیف کنیم. برای این کار٬ یک آرایه به نام tools ایجاد می کنیم که شامل یک شیء برای هر تابع است. هر شیء دو کلید خواهد داشت: type, function و کلید function سه زیرکلید دارد: name, description و parameters.

در زیر نحوه توصیف هر یک از توابع که شامل اسم تابع٬ آرگومان‌های ورودی و توضیحی در مورد عملکرد تابع است را مشاهده می‌کنید.

 1const tools = [
 2  {
 3    type: "function",
 4    function: {
 5      name: "getCurrentWeather",
 6      description: "Get the current weather in a given location",
 7      parameters: {
 8        type: "object",
 9        properties: {
10          latitude: {
11            type: "string",
12          },
13          longitude: {
14            type: "string",
15          },
16        },
17        required: ["longitude", "latitude"],
18      },
19    }
20  },
21  {
22    type: "function",
23    function: {
24      name: "getLocation",
25      description: "Get the user's location based on their IP address",
26      parameters: {
27        type: "object",
28        properties: {},
29      },
30    }
31  },
32];

ساخت آرایه‌ای از پیام‌ها #

ما همچنین باید یک آرایه messages تعریف کنیم. این آرایه همه پیام های رفت و برگشت بین برنامه ما و مدل را پیگیری می کند. اولین شیء در آرایه باید همیشه یک پیغام سیستمی باشد تا به مدل بگوید چگونه رفتار کند.

1const messages = [
2  {
3    role: "system",
4    content:
5      "You are a helpful assistant. Only use the functions you have been provided with.",
6  },
7];

ساخت تابع agent #

اکنون آماده ایم تا منطق برنامه خود را که در تابع agent قرار دارد ، بسازیم. این تابع یک آرگومان به نام userInput را می گیرد.

کار را با افزودن userInput به آرایه پیام ها شروع می کنیم. این بار، role این پیام را برابر با user قرار می‌دهیم تا مدل بداند که این ورودی از کاربر است.

 1async function agent(userInput) {
 2  messages.push({
 3    role: "user",
 4    content: userInput,
 5  });
 6  const response = await openai.chat.completions.create({
 7    model: "gpt-4-turbo",
 8    messages: messages,
 9    tools: tools,
10  });
11  console.log(response);
12}

سپس، یک درخواست به اندپوینت Chat completions از طریق متود chat.completions.create() در SDK Node ارسال می کنیم.

این متود یک شیء با مقادیر زیر را به عنوان آرگومان می گیرد.

  • model: تصمیم می گیرد که از کدام مدل GPT استفاده کند (در مورد اینجا gpt-4-turbo).
  • messages: تاریخچه کامل پیام ها بین کاربر و مدل تا این لحظه.
  • tools: لیستی از ابزارهایی که مدل ممکن است فراخوانی کند. در حال حاضر، فقط توابع به عنوان یک ابزار پشتیبانی می شوند.

اجرای برنامه #

حال می‌خواهیم سعی کنیم تا agent را با یک ورودی که نیاز به یک فراخوانی تابع برای دادن پاسخ مناسب به سوال ما را دارد، اجرا کنیم.

1agent("محل جغرافیایی من کجاست؟");

با اجرای کد بالا پاسخ زیر در کنسول چاپ می‌شود.

 1{
 2    id: "chatcmpl-84ojoEJtyGnR6jRHK2Dl4zTtwsa7O",
 3    object: "chat.completion",
 4    created: 1696159040,
 5    model: "gpt-4-turbo",
 6    choices: [{
 7        index: 0,
 8        message: {
 9            role: "assistant",
10            content: null,
11            tool_calls: [
12              id: "call_CBwbo9qoXUn1kTR5pPuv6vR1",
13              type: "function",
14              function: {
15                name: "getLocation",
16                arguments: "{}"
17              }
18            ]
19        },
20        logprobs: null,
21        finish_reason: "tool_calls" // Model wants us to call a function
22    }],
23    usage: {
24        prompt_tokens: 134,
25        completion_tokens: 6,
26        total_tokens: 140
27    }
28     system_fingerprint: null
29}

این پاسخ به ما می گوید که برنامه باید یکی از توابع خود را فراخوانی کند، زیرا مقدار: finish_reason برابر با tool_calls است. نام تابع در کلید response.choices[0].message.tool_calls[0].function.name قرار دارد که برابر با getLocation ٬تابعی از برنامه که ما به مدل معرفی کرده‌ایم٬ است.

فراخوانی تابع در داخل برنامه #

حال که مدل تابع متناسب با ورودی کاربر که باید فراخوانی شود را به برگرداند٬ باید آن را در داخل برنامه فراخوانی کنیم.

برای این کار نام توابعی که به مدل معرفی کرده ایم را در یک ثابت به نام availableTools ذخیره می‌کنیم.

1const availableTools = {
2  getCurrentWeather,
3  getLocation,
4};

و حال با استفاده از نام تابع برگردانده شده از سوی مدل می‌توانیم تابع اصلی را پیدا کرده و آن را با پارامترهای مورد نیاز که آنها هم توسط مدل تولید شده اند فراخوانی کنیم.

البته در این جا هنوز نیازی به فراخوانی تابع با مقادیر آرگومان‌ها نیست.

 1const { finish_reason, message } = response.choices[0];
 2 
 3if (finish_reason === "tool_calls" && message.tool_calls) {
 4  const functionName = message.tool_calls[0].function.name;
 5  const functionToCall = availableTools[functionName];
 6  const functionArgs = JSON.parse(message.tool_calls[0].function.arguments);
 7  const functionArgsArr = Object.values(functionArgs);
 8  const functionResponse = await functionToCall.apply(null, functionArgsArr);
 9  console.log(functionResponse);
10}

خروجی برای یوزر ما:

{ip: "193.212.60.170", network: "193.212.60.0/23", version: "IPv4", city: "Oslo", region: "Oslo County", region_code: "03", country: "NO", country_name: "Norway", country_code: "NO", country_code_iso3: "NOR", country_capital: "Oslo", country_tld: ".no", continent_code: "EU", in_eu: false, postal: "0026", latitude: 59.955, longitude: 10.859, timezone: "Europe/Oslo", utc_offset: "+0200", country_calling_code: "+47", currency: "NOK", currency_name: "Krone", languages: "no,nb,nn,se,fi", country_area: 324220, country_population: 5314336, asn: "AS2119", org: "Telenor Norge AS"}

حال این داده ها را به یک پیغام جدید در آرایه messages اضافه می کنیم، جایی که نام تابعی را که فراخوانی کردیم را نیز مشخص می کنیم

1messages.push({
2  role: "function",
3  name: functionName,
4  content: `The result of the last function was this: ${JSON.stringify(
5    functionResponse
6  )}
7  `,
8});

توجه داشته باشید که role به function تغییر کرده است. این به مدل می گوید که پارامتر content نتیجه فراخوانی تابع را دارد و نه ورودی کاربر را.

حال، ما باید یک درخواست جدید به مدل با این آرایه messages به روز شده ارسال کنیم تا مدل با استفاده داده های جدید مجددا سعی کند که به سوال کاربر پاسخ دهد. برای این کار بهتر است یک حلقه ایجاد کنیم تا این رفت و برگشت بین agent و مدل را برای چند دفعه انجام دهد. انتظار ما این است که مدل نهایتا بتواند سوال کاربر را به درستی جواب دهد.

گد پایین شامل یک حلقه است که به برنامه اجازه می‌ده تا کل روند را تا پنج بار اجرا کند. اگر ما finish_reason: "tool_calls" را از مدل دریافت کنیم، فقط نتیجه فراخوانی تابع را به آرایه messages اضافه می کنیم و به تکرار بعدی حلقه می رویم. در صورتی که finish_reason: "stop" را دریافت کنیم، در این صورت معنی اش این است که مدل پاسخ مناسب را پیدا کرده است، بنابراین می توانیماز حلقه خارج شویم .

 1for (let i = 0; i < 5; i++) {
 2  const response = await openai.chat.completions.create({
 3    model: "gpt-4",
 4    messages: messages,
 5    tools: tools,
 6  });
 7  const { finish_reason, message } = response.choices[0];
 8 
 9  if (finish_reason === "tool_calls" && message.tool_calls) {
10    const functionName = message.tool_calls[0].function.name;
11    const functionToCall = availableTools[functionName];
12    const functionArgs = JSON.parse(message.tool_calls[0].function.arguments);
13    const functionArgsArr = Object.values(functionArgs);
14    const functionResponse = await functionToCall.apply(null, functionArgsArr);
15 
16    messages.push({
17      role: "function",
18      name: functionName,
19      content: `
20          The result of the last function was this: ${JSON.stringify(
21            functionResponse
22          )}
23          `,
24    });
25  } else if (finish_reason === "stop") {
26    messages.push(message);
27    return message.content;
28  }
29}
30return "The maximum number of iterations has been met without a suitable answer. Please try again with a more specific input.";

اجرای برنامه‌ی نهایی #

حالا آماده امتحان کردن برنامه‌ی نهایی هستیم. من از agent خواهم خواست که بر اساس مکان من و آب و هوای فعلی، چند فعالیت مناسب را پیشنهاد کند.

1const response = await agent(
2  "لطفا بر اساس موقعیت جغرافیایی من و آب و هوای فعلی جند فعالیت مناسب را پیشنهاد بده."
3);
4console.log(response);

و این جوابی هست که از برنامه می‌گیرم.

Based on your current location in Oslo, Norway and the weather (15°C and snowy),
here are some activity suggestions:
 
1. A visit to the Oslo Winter Park for skiing or snowboarding.
2. Enjoy a cosy day at a local café or restaurant.
3. Visit one of Oslo's many museums. The Fram Museum or Viking Ship Museum offer interesting insights into Norway’s seafaring history.
4. Take a stroll in the snowy streets and enjoy the beautiful winter landscape.
5. Enjoy a nice book by the fireplace in a local library.
6. Take a fjord sightseeing cruise to enjoy the snowy landscapes.
 
Always remember to bundle up and stay warm. Enjoy your day!

اگر نگاهی به لیست پیام‌های رد و بدل شده بین agent و مدل بیاندازیم - response.choices[0].message - می بینیم که مدل به برنامه دستور اجرای هر دو تابع را داده است. ابتدا، دستور اجرای تابع getLocation و سپس تابع getCurrentWeather را با “longitude”: “10.859”, “latitude”: “59.955” به عنوان آرگومان ها.

این داده ای است که از اولین فراخوانی تابعی که انجام دادیم، برگردانده شده است.

{"role":"assistant","content":null,"tool_calls":[{"id":"call_Cn1KH8mtHQ2AMbyNwNJTweEP","type":"function","function":{"name":"getLocation","arguments":"{}"}}]}
{"role":"assistant","content":null,"tool_calls":[{"id":"call_uc1oozJfGTvYEfIzzcsfXfOl","type":"function","function":{"name":"getCurrentWeather","arguments":"{\n\"latitude\": \"10.859\",\n\"longitude\": \"59.955\"\n}"}}]}

تبریک! شما توانستید یک عامل AI را که قادر به انجام کارهایی که کاربر از و می‌خواهد٬ است را ساختید.

اگر به دنبال چالش بیشتری هستید، می‌توانید دامنه کارهایی را که agent شما از پس انجام آنها بر میاد را با تعریف توابع جدید بهبود دهید.