Exploring the Code

In this chapter, we will be exploring the code of the project and discussing the architecture of the chatbot. We will also be looking into the most important files of the project.

Project Structure

The project is structured as follows:

bash
├── .gitignore
├── README.md
├── credential.js
├── deploy-local-ledger.sh
├── dfx.json
├── package-lock.json
├── package.json
├── tsconfig.json
├── webpack.config.js
src/
    dfinity_js_backend/
    dfinity_js_frontend/

The most important files and directories are:

  • credential.js: Contains the OpenAI API key and assistant ID. We have created this file in the previous chapter.
  • deploy-local-ledger.sh: A shell script for deploying the canisters to the local network.
  • dfx.json: The configuration file for DFX. It contains the configuration for the canisters and the local network.
  • dfinity_js_backend: The directory containing the source code of the backend canister.
  • dfinity_js_frontend: The directory containing the source code of the frontend canister.

Backend Canister

The backend canister is responsible for communicating with the OpenAI API and providing the frontend canister with the response. It is written in TypeScript and uses the DFINITY Canister SDK to communicate with the Internet Computer Protocol. The backend canister is located in the dfinity_js_backend directory.

The structure of the backend canister is as follows:

bash
src/
    dfinity_js_backend/
        ├── assistant.ts
        ├── index.did
        ├── index.ts
        ├── user.ts
        models/
            ├── assistant.ts
            ├── error.ts

assistant.ts

We are looking at the assistant.ts file located in the dfinity_js_backend directory. This file contains the code for the Assistant class, which is responsible for managing the assistant and its interactions with the user. The class contains methods for retrieving the assistant's ID, saving a thread, retrieving a thread, and deleting a thread. It also contains methods for checking if a thread exists for a given user identity.

typescript
import {
  update,
  text,
  Ok,
  Err,
  Result,
  StableBTreeMap,
  query,
  bool,
} from "azle";
import { ErrorResponse } from "./models/error";
import { CreateThead, Thread } from "./models/assistant";

The file starts by importing necessary functions and types from azle, a library for building decentralized applications on the Internet Computer Protocol in TypeScript. It also imports models such as ErrorResponse and thread-related structures (CreateThead, Thread) for data handling.

typescript
const threadStorage = StableBTreeMap(text, CreateThead, 4);

Next, the file initializes threadStorage as a StableBTreeMap, a data structure used for storing threads. The map is keyed by text and contains CreateThead values. The map is initialized with a capacity of 4.

typescript
class Assistant {
  getAssistant(assistantId: string) {
    return query([], Result(text, ErrorResponse), () => {
      return Ok(assistantId);
    });
  }

The Assistant class encapsulates various methods for managing threads and interactions with the assistant. The getAssistant method retrieves an assistant's ID. It uses a query function, indicating that it fetches data without modifying the state.

typescript
  saveThread() {
    return update(
      [text, Thread],
      Result(Thread, ErrorResponse),
      async (userIdentity, thread) => {
        if (!userIdentity || !thread || typeof thread !== "object") {
          return Err({
            error: { message: "userIdentity and thread can not be empty" },
          });
        }
 
       // Support one thread for now, can add multiple threads support
        const hasASavedThread = this.hasASavedThread_(userIdentity);
        if (hasASavedThread) {
          const thread = threadStorage.get(userIdentity.trim());
          return Ok(thread.Some.thread);
        }
 
        const threadToSave: typeof CreateThead = {
          thread,
        };
 
        threadStorage.insert(userIdentity, threadToSave);
        return Ok(threadToSave.thread);
      }
    )
  }

The saveThread method is responsible for saving a thread. It checks whether a saved thread exists for the given userIdentity by calling the this.hasASavedThread_ method. If a saved thread is found, it retrieves and returns the thread.If no saved thread is found, the code prepares a threadToSave object of type CreateThead, wraps it in an object, and inserts it into threadStorage using threadStorage.insert(userIdentity, threadToSave).

There are additional methods for retrieving, and deleting threads, we will not be going over them in this tutorial.

index.did

This file contains the interface for the backend canister. It defines the service provided by the canister and the methods it exposes. The file is written in Candid, an interface description language for the Internet Computer Protocol. This file automatically generated by DFX when you deploy the canister. If you want to learn more follow the Typescript Smart Contract 101 tutorial here.

index.ts

The index.ts file contains the code for the backend canister. This file is responsible for initializing the canister and exposing the service defined in the index.did file.

user.ts

The user.ts file contains the code for the User class, which is responsible for managing user identities. The class contains methods for retrieving a username and updating a username. It also contains a method for checking if a username exists for a given user identity.

models/assistant.ts

The assistant.ts file contains models for data handling. The file contains models for saving an assistant, a thread, and creating a thread. These models are used by the Assistant class.

models/error.ts

The error.ts file contains the ErrorResponse model, which is used for returning error messages. The model is used by the Assistant class.

Frontend Canister

The frontend canister is responsible for providing the user interface for the chatbot. It is written in TypeScript and uses the DFINITY Canister SDK to communicate with the Internet Computer Protocol. The frontend canister is located in the dfinity_js_frontend directory.

The structure of the frontend canister is as follows:

bash
        dfinity_js_frontend/
            ...
            src/
                ├── App.js
                ├── index.js
                ...
                components/
                ...
                context/
                    ├── assistantProvider.js
                    ├── userProvider.js
                utils/
                    ├── assistantCanister.js
                    ├── auth.js
                    ├── canisterFactory.js
                    ├── chat.js
                    ├── icp.js
                    ├── localStorageController.js

The most important files and directories are:

  • src/App.js: The main file of the frontend canister. It contains the code for the chatbot's user interface.
  • src/index.js: The entry point of the frontend canister. It contains the code for initializing the canister and rendering the chatbot's user interface.
  • src/components: The directory containing the React components of the chatbot. We will not be going over them here, they should be pretty straightforward.
  • src/context: The directory containing the React contexts of the chatbot. We will be going over them in the next section.
  • src/utils: The directory containing utility functions for the chatbot. We will be going over them in a later section.

context/assistantProvider.js

The assistantProvider.js file contains the code for the AssistantProvider component, which is responsible for managing the assistant and its interactions with the user. It uses the AssistantContext context to provide the assistant and thread to its children. It also uses the UserContext context to retrieve the user identity.

context/userProvider.js

The userProvider.js file contains the code for the UserProvider component, which is responsible for managing the user identity and its interactions with the assistant. It uses the UserContext context to provide the user identity to its children.

utils/assistantCanister.js

The assistantCanister.js file contains functions for communicating with the backend canister. It uses the window.canister object to access the backend canister. The file contains functions for retrieving the assistant, saving a thread, retrieving a thread, deleting a thread, and checking if a thread exists for a given user identity.

utils/auth.js

The auth.js file contains functions for managing the user's authentication. It uses the @dfinity/auth-client library to handle authentication. The file contains functions for logging in and logging out.

utils/canisterFactory.js

The canisterFactory.js file contains functions for creating canister actors. It uses the @dfinity/agent library to create canister actors. The file contains a function for creating a canister actor for the chatbot canister. A canister actor is an object that allows you to call methods on a canister.

utils/chat.js

The chat.js file contains functions for communicating with the OpenAI API. It uses the openai library to communicate with the OpenAI API. The file contains functions for creating a thread, saving a thread, retrieving a thread, creating a message, retrieving all messages, and analyzing runs steps.

typescript
const openai = new Openai({
  apiKey: OPEN_AI_API_KEY,
  dangerouslyAllowBrowser: true,
});

We are using the openai library to communicate with the OpenAI API. The library is initialized with the OpenAI API key and the dangerouslyAllowBrowser flag set to true. This flag allows the library to be used in the browser, which is not recommended in a production environment.

typescript
export const runTheAssistantOnTheThread = async (threadId, assistantId) => {
  try {
    const run = await openai.beta.threads.runs.create(threadId, {
      assistant_id: assistantId,
    });
    return run.id;
  } catch (e) {
    console.log(e);
  }
};

The runTheAssistantOnTheThread function runs the assistant on a given thread. It takes a thread ID and an assistant ID as inputs. It uses the openai.beta.threads.runs.create function to run the assistant on the thread. It then returns the run ID. Which is needed to retrieve the assistant's response.

We then have functions for creating a thread, saving a thread, retrieving a thread, creating a message, retrieving all messages, and analyzing runs steps.

typescript
export const analyseRunsStepsDone = async (threadId, runId) => {
  const runStep = await openai.beta.threads.runs.steps.list(threadId, runId);
  const completedStep = runStep.data.find(
    (step) => step.status === "completed"
  );
 
  if (completedStep) {
    return true;
  } else {
    await new Promise((resolve) => setTimeout(resolve, 3000));
    return await analyseRunsStepsDone(threadId, runId);
  }
};

The analyseRunsStepsDone function analyzes the steps of a run. It takes a thread ID and a run ID as inputs. It uses the openai.beta.threads.runs.steps.list function to retrieve the steps of the run. It then checks if the run is completed. If it is, it returns true. If not, it waits for 3 seconds and calls itself again.

typescript
export const retreiveAssistantFromOpenai = async (assistantid) => {
  const assistant = await openai.beta.assistants.retrieve(assistantid);
  return assistant;
};

The retreiveAssistantFromOpenai function retrieves an assistant from the OpenAI API. It takes an assistant ID as input. It uses the openai.beta.assistants.retrieve function to retrieve the assistant. It then returns the assistant.

utils/icp.js

The icp.js file contains functions for initializing the canister and the authentication client. It uses the @dfinity/agent library to create canister actors. The file contains a function for creating a canister actor for the chatbot canister. A canister actor is an object that allows you to call methods on a canister.

utils/localStorageController.js

The localStorageController.js file contains functions for managing the local storage. It contains a function for retrieving data from the local storage and a function for saving data to the local storage.

Next Chapter

Conclusion
ICP CHAT BOT

👋 Welcome! If you need assistance with any part of this tutorial, get stuck or have questions about the material, I'm here to help.