跳转到主要内容

category

 

自然语言处理(NLP)的兴起为开发人员提供了令人兴奋的机会,使他们能够创建引人入胜的交互式聊天机器人应用程序。

在这篇博客文章中,我们将指导您使用TypeScript编程语言从头开始创建聊天机器人应用程序。该项目将基于最新版本的前端Angular框架,我们将使用NestJS通过现有的Gemini API聊天和文本生成功能创建API服务。

What is Gemini API?

Gemini API为开发人员提供了一个强大的工具,用于构建通过自然语言与世界交互的应用程序。API实现充当到大型语言模型(LLM)的桥梁,允许您访问自然语言处理(NLP)功能。

有了Gemini,你可以:

  • 生成富有创意的文本:摘要、翻译、代码、模板电子邮件、信件等等。
  • 回答用户的问题:对复杂的查询提供深刻而全面的回答。
  • 参与对话:在有意义的背景下,参与自然感觉的讨论。

Gemini模型是为多模态从头开始构建的,这意味着它们可以帮助进行文本、图像、音频、视频和代码的推理。

Learn more about Gemini here.

Get an API Key for Gemini API

Before getting started with code, you’ll need to generate an API Key for the project. This needs to be done on Google AI Studio.

Once you get access to Google AI Studio, click on the “Get API Key” button on the left side to be redirected to the API keys view. Then, click on the “Create API Key” button to generate your first API key.

 
Google AI Studio: Create an API Key

As you may find on the same page, the quickest way to test the API using the brand-new API key is by running the following cURL command on your terminal:

curl \
  -H 'Content-Type: application/json' \
  -d '{"contents":[{"parts":[{"text":"Write a story about a magic backpack"}]}]}' \
  -X POST https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key=YOUR_API_KEY

Make sure to save the API key in a safe place since we’ll use it as an environment variable later.

⚠️ This API Key should not be versioned along with your source code! ⚠️

Create the Project Repository

In this case, we’ll use Nx to create the project repository from scratch. This will be a mono repository with the support of two applications: A client app based on Angular and a server app based on NestJS.

npx create-nx-workspace@latest --preset apps \
--name gemini-angular-nestjs \
--nxCloud skip

That’s a fast way to create an initial Nx workspace. You can see more options for create-nx-workspace here.

Create a Shared Model library

One practical way to define the shared code between apps is a new library. Let’s create a data-model library to define the common types for the project.

nx generate @nx/js:library --name=data-model \
 --unitTestRunner=none \
 --directory=libs/data-model \
 --importPath=data-models \
 --publishable=true \
 --projectNameAndRootFormat=as-provided \

Next, let’s define a chat-content.ts file under libs/data-model/src/lib as follows:

export interface ChatContent {
    agent: 'user' | 'chatbot';
    message: string;
}

That will be the base model to be used on the future Angular app and NestJS code.

Create the Server Application

It’s time to create the server folder as part of the new Nx workspace. Let’s enter the newly new created folder:

cd gemini-angular-nestjs

Next, let’s install the NestJS application schematics:

npm install --save-dev @nx/nest

Now we can move forward creating the NestJS application using the following command:

nx generate @nx/nest:application \
--name server \
--e2eTestRunner none 

The parameter name sets the name of the application where the NestJS code will be implemented. You can find other options for NestJS apps here.

After finishing the previous command, run the server application.

nx serve server

Set the Environment Variable

Create a .env file under the brand-new server folder. The environment file should have the API key you just created following the next example:

API_KEY=$YOUR_API_KEY

Creating the Chat and Text Services

Initially, we’ll add the support of text generation and multi-turn conversations(chat) using the Gemini API.

For a Node.js context, you need to install the GoogleGenerativeAI package:

npm install @google/generative-ai

Let’s create two services to handle both functionalities:

nx generate @nx/nest:service --name=chat
nx generate @nx/nest:service --name=text

Open the chat.service.ts file and put the following code:

import { Injectable } from '@nestjs/common';
import {
  ChatSession,
  GenerativeModel,
  GoogleGenerativeAI,
} from '@google/generative-ai';

import { ChatContent } from 'data-model';

@Injectable()
export class ChatService {
  model: GenerativeModel;
  chatSession: ChatSession;
  constructor() {
    const genAI = new GoogleGenerativeAI(process.env.API_KEY);
    this.model = genAI.getGenerativeModel({ model: 'gemini-pro' });
    this.chatSession = this.model.startChat({
      history: [
        {
          role: 'user',
          parts: `You're a poet. Respond to all questions with a rhyming poem.
            What is the capital of California?
          `,
        },
        {
          role: 'model',
          parts:
            'If the capital of California is what you seek, Sacramento is where you ought to peek.',
        },
      ],
    });
  }

  async chat(chatContent: ChatContent): Promise<ChatContent> {
    const result = await this.chatSession.sendMessage(chatContent.message);
    const response = await result.response;
    const text = response.text();

    return {
      message: text,
      agent: 'chatbot',
    };
  }
}

The class ChatService defines a model created through GoogleGenerativeAI constructor that needs to read the API Key.

The ChatSession object is needed to handle the multi-turn conversation. This object will store the conversation history for you. In order to initialize the chat, you can use the startChat() method and then set the initial context using the history property, which contains the first messages based on two roles: user and model.

The class method chat will take the chatContent object to register a new message from the user through sendMessage call. Then, the text value is extracted at the end before returning a ChatContent object for the client application.

Now, open the text.service.ts file and put the following content:

import { Injectable } from '@nestjs/common';
import {
    GenerativeModel,
    GoogleGenerativeAI,
  } from '@google/generative-ai';
import { ChatContent } from 'data-model';

@Injectable()
export class TextService {
    model: GenerativeModel;

    constructor() {
        const genAI = new GoogleGenerativeAI(process.env.API_KEY);
        this.model = genAI.getGenerativeModel({ model: "gemini-pro"});
    }

    async generateText(message: string): Promise<ChatContent> {
        const result = await this.model.generateContent(message);
        const response = await result.response;
        const text = response.text();
    
        return {
          message: text,
          agent: 'chatbot',
        };
      }
}

The TextService class creates the model property through the GoogleGenerativeAI class that requires the API key. Observe that the gemini-pro model is used here too since it’s optimized for multi-turn converstaions and text-only input as the use case.

The generateText method will take the chatContent object to extract the text message and use the generateContent method from the model.

For the text generation use case, there’s no need to handle history as we did for the multi-turn conversations(chat service).

Update the App Controller

We already defined the code that uses the Gemini model for text generation and chat. Let’s create the POST endpoints for the backend application.

To do that, open the app.controller.ts file and set the following content:

// app.controller.ts

import { Controller, Get, Post, Body } from '@nestjs/common';

import { ChatContent } from 'data-model';
import { ChatService } from './chat.service';
import { TextService } from './text.service';

@Controller()
export class AppController {
  
  constructor(private readonly chatService: ChatService, private readonly textService: TextService) {}

  @Post('chat')
  chat(@Body() chatContent: ChatContent) {
    return this.chatService.chat(chatContent);
  }
  
  @Post('text')
  text(@Body() chatContent: ChatContent) {
    return this.textService.generateText(chatContent.message);
  }
}

The AppController class injects the ChatService and the TextService we created before. Then, it uses the @Post decorators to create the /api/chat and the /api/text endpoints.

Create the Client Application

For the frontend application, we’ll need to create a client folder as part of the Nx workspace. First, let’s install the Angular application schematics:

npm install --save-dev @nx/angular

Now, let’s create the Angular app with the following command:

nx generate @nx/angular:application client \
--style scss \
--prefix corp \
--routing \
--skipTests true \
--ssr false \
--bundler esbuild \
--e2eTestRunner none

The client application will use SCSS for styling and set the prefix for components as corp. You may find more details about the all options available here.

You can run the client application as follows:

nx serve client

Adding Angular Material

Let’s implement the UI using Angular Material components. We can add that support running the following commands:

npm install @angular/material
nx g @angular/material:ng-add --project=client

The latest command will perform changes on the project configuration while setting up the needed styles for Angular Material. Pay attention to the output to understand what changes were made in the project.

Create the Gemini Service

Before starting to implement the components, let’s create an Angular service to manage the HTTP communication for the client app.

nx generate @schematics/angular:service --name=gemini --project=client --skipTests=true

Open the gemini.service.ts file and put the following code:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ClientChatContent } from './client-chat-content';

@Injectable({
  providedIn: 'root',
})
export class GeminiService {
  constructor(private httpClient: HttpClient) { }

  chat(chatContent: ClientChatContent): Observable<ClientChatContent> {
    return this.httpClient.post<ClientChatContent>('http://localhost:3000/api/chat', chatContent);
  }

  generateText(message: string): Observable<ClientChatContent> {
    return this.httpClient.post<ClientChatContent>('http://localhost:3000/api/text', {message});
  }
}

The GeminiService class has two methods: chat and generateText.

  • chat is used to send a chat message to the server. The chat message is sent as the body of the request.
  • generateText is used to generate text based on a given prompt.

Create the Chat and Text Components

The Chat Component

It’s time to create the components needed for the text generation and the chat. Let’s start with the chat component:

npx nx generate @nx/angular:component \
--name=chat \
--directory=chat 
--skipTests=true \
--style=scss

Open the chat.component.ts file and set the following TypeScript code:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';

import { GeminiService } from '../gemini.service';
import { LineBreakPipe } from '../line-break.pipe';
import { finalize } from 'rxjs';
import { ClientChatContent } from '../client-chat-content';


@Component({
  selector: 'corp-chat',
  standalone: true,
  imports: [
    CommonModule,
    MatIconModule,
    MatInputModule,
    MatButtonModule,
    MatFormFieldModule,
    FormsModule,
    LineBreakPipe,
  ],
  templateUrl: './chat.component.html',
  styleUrls: ['./chat.component.scss']
})
export class ChatComponent {
  message = '';

  contents: ClientChatContent[] = [];

  constructor(private geminiService: GeminiService) {}

  sendMessage(message: string): void {
    const chatContent: ClientChatContent = {
      agent: 'user',
      message,
    };

    this.contents.push(chatContent);
    this.contents.push({
      agent: 'chatbot',
      message: '...',
      loading: true,
    });
    
    this.message = '';
    this.geminiService
      .chat(chatContent)
      .pipe(
        finalize(() => {
          const loadingMessageIndex = this.contents.findIndex(
            (content) => content.loading
          );
          if (loadingMessageIndex !== -1) {
            this.contents.splice(loadingMessageIndex, 1);
          }
        })
      )
      .subscribe((content) => {
        this.contents.push(content);
      });
  }
}

The ChatComponent class defines two properties: message and contents. The input field defined in the template will bind to the message property since it’s a string value. The constructor of the class injects an instance of GeminiService into the component. The GeminiService class is used to handle the chat functionality.

The sendMessage method is used to send a message to the API and a loading message is rendered while the request is in progress. Once a message is sent, it is added to the chat history.

Once the TypeScript logic is set, open the chat.component.html file and put the following code:

<div class="chat-container">
  <div class="message-container" *ngIf="contents.length === 0">
    <p class="message">
      Welcome to your Gemini ChatBot App <br />
      Write a text to start.
    </p>
  </div>
  <div
    *ngFor="let content of contents"
    class="chat-message"
    [ngClass]="content.agent"
  >
    <img [src]="'assets/avatar-' + content.agent + '.png'" class="avatar" />
    <div class="message-details">
      <p
        class="message-content"
        [ngClass]="{ loading: content.loading }"
        [innerHTML]="content.message | lineBreak"
      ></p>
    </div>
  </div>
</div>

<div class="chat-footer-container">
  <mat-form-field class="chat-input">
    <input
      placeholder="Send a message"
      matInput
      #inputMessage
      [(ngModel)]="message"
      (keyup.enter)="sendMessage(message)"
    />
  </mat-form-field>
  <button mat-icon-button color="primary" (click)="sendMessage(message)">
    <mat-icon>send</mat-icon>
  </button>
</div>

The template defines the HTML structure and layout for the chat interface. A welcome message is displayed when there are no messages yet.

The main chat container section will display each message through the ngFor directive based on the contents array.

The chat footer container is the place where the user can input and send messages.

 
Gemini and a Chatbot Application

The Text Component

Let’s create the component for the text generation:

npx nx generate @nx/angular:component \
--name=text \
--directory=text 
--skipTests=true \
--style=scss

Open the text.component.ts file and put the following code:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';

import { MarkdownModule } from 'ngx-markdown';

import { GeminiService } from '../gemini.service';
import { ClientChatContent } from '../client-chat-content';
import { LineBreakPipe } from '../line-break.pipe';
import { finalize } from 'rxjs';


@Component({
  selector: 'corp-text',
  standalone: true,
  imports: [
    CommonModule,
    MatIconModule,
    MatInputModule,
    MatButtonModule,
    MatFormFieldModule,
    FormsModule,
    LineBreakPipe,
    MarkdownModule
  ],
  templateUrl: './text.component.html',
  styleUrls: ['./text.component.scss']
})
export class TextComponent {
  message = '';
  contents: ClientChatContent[] = [];

  constructor(private geminiService: GeminiService) {}

  generateText(message: string): void {
    const chatContent: ClientChatContent = {
      agent: 'user',
      message,
    };

    this.contents.push(chatContent);
    this.contents.push({
      agent: 'chatbot',
      message: '...',
      loading: true,
    });

    this.message = '';
    this.geminiService
      .generateText(message)
      .pipe(
        finalize(() => {
          const loadingMessageIndex = this.contents.findIndex(
            (content) => content.loading
          );
          if (loadingMessageIndex !== -1) {
            this.contents.splice(loadingMessageIndex, 1);
          }
        })
      )
      .subscribe((content) => {
        this.contents.push(content);
      });
  }
}

The previous code defines an Angular component called TextComponent. It has two properties message and contents as the ChatComponent does. The behavior is similar and the only difference here is the generateText method is used to generate text based on a given message(user’s prompt).

Next, open the text.component.html file and set the following code:

<div class="chat-container">
  <div class="message-container" *ngIf="contents.length === 0">
    <p class="message">
      Welcome to your Gemini App <br />
      Write an instruction to start.
    </p>
  </div>
  <div
    *ngFor="let content of contents"
    class="chat-message"
    [ngClass]="content.agent"
  >
    <img [src]="'assets/avatar-' + content.agent + '.png'" class="avatar" />
    <div class="message-details">
      <p *ngIf="content.loading"
        class="message-content"
        [ngClass]="{ loading: content.loading }"
        [innerHTML]="content.message | lineBreak"
      ></p>
      <markdown *ngIf="!content.loading"
        class="variable-binding message-content"
        [data]="content.message"
      ></markdown>
    </div>
  </div>
</div>

<div class="chat-footer-container">
  <mat-form-field class="chat-input">
    <mat-label>Send a message</mat-label>
    <textarea
      matInput
      #inputMessage
      [(ngModel)]="message"
      (keyup.enter)="generateText(message)"
    ></textarea>
  </mat-form-field>
  <button mat-icon-button color="primary" (click)="generateText(message)">
    <mat-icon>send</mat-icon>
  </button>
</div>

This template is very similar to the Chat component. However, it uses the markdown element to render code and text formatting that may come as part of the generated text.

To make it work, we’ll need to install the ngx-markdown and marked packages:

npm install --save ngx-markdown marked

Also, you will need to update the app.config.ts file and import the MarkdownModule support:


import { MarkdownModule } from 'ngx-markdown';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(appRoutes), 
    provideAnimationsAsync(),
    provideHttpClient(),
    importProvidersFrom([
      MarkdownModule.forRoot()
  ])
  ],
};
 
Gemini and a Text generation Application

Source Code of the Project

Find the complete project in this GitHub repository: gemini-angular-nestjs. Do not forget to give it a star ⭐️ and play around with the code.

Conclusion

In this step-by-step tutorial, we demonstrated how to build a web application from scratch with the ability to generate text and have multi-turn conversations using the Gemini API. The Nx workspace is ready to add shared code, or even add more applications as the project grows.