Hoy veo muchos chats conectados con IA, un amigo una vez dijo que es difícil, pero no lo es. Hoy, quiero demostrar lo fácil que es construir tu propio chat conectado con IA y una interfaz agradable usando solo unas pocas líneas de código.

Aprenderemos cómo crear una aplicación Angular usando la biblioteca Gemini e integrarla con Kendo Conversational UI para una interfaz de usuario impresionante.

¿Cuánto tiempo tomará? Sorprendentemente, se puede hacer rápidamente. ¡Empecemos!

Recuerda que si tu quieres prácticar tu inglés puedes visitar mi blog

Configurar Proyecto

Primero, crea la aplicación Angular e instala la biblioteca Gemini usando Angular CLI con el comando ng new conversional-with-gemini. Esto genera un nuevo proyecto Angular vacío.


ng new conversional-with-gemini

Navega al directorio y genera un servicio Gemini-client usando el generador CLI ejecutando el siguiente comando:

ng g s services/gemini
CREATE src/app/services/gemini.service.spec.ts (404 bytes)
CREATE src/app/services/gemini.service.ts (150 bytes)

En Angular 17, ya que no tenemos el archivo de entorno, créalo usando el comando ng generate environments.


ng generate environments

Usando Gemini

Para usar Gemini, necesitamos utilizar el SDK de Gemini con una clave. Primero, instala el SDK de Gemini en el proyecto ejecutando el siguiente comando:


npm i @google/generative-ai

Luego, obtén tu clave API desde https://ai.google.dev/, cópiala y almacena el valor en el archivo environment.ts, así:

export const environment = {
  geminy_key: "YourAmazingKey"
};

Tenemos la clave y el SDK instalados; ahora es el momento de usarlos en nuestro servicio de cliente Gemini. ¡Hagámoslo!

Usando el SDK de Gemini

Vamos a usar el SDK de Gemini en el servicio gemini-client, creando una instancia de Google AI con la clave y generando un modelo, en nuestro caso, 'gemini-pro', que devuelve una instancia del modelo lista para trabajar.

En nuestro caso, queremos usar el modelo basado en un prompt. Para mis necesidades específicas, quiero que funcione como un desarrollador Angular con experiencia en Kendo UI para Angular.

CPU
1 vCPU
MEMORIA
1 GB
ALMACENAMIENTO
10 GB
TRANSFERENCIA
1 TB
PRECIO
$ 4 mes
Para obtener el servidor GRATIS debes de escribir el cupon "LEIFER"

Entonces mi prompt será como:

Eres un experto en la biblioteca Kendo UI para Angular, proporciona un escenario real y cómo este producto ayuda a resolver, con ejemplos de código angular y enlaces para recursos

El código debe verse así:

import { Injectable } from '@angular/core';
import { GoogleGenerativeAI } from '@google/generative-ai';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root',
})
export class GeminiService {
  #generativeAI = new GoogleGenerativeAI(environment.geminy_key);

  #prompt =
    'Eres un experto en la biblioteca Kendo UI para Angular, proporciona un escenario real y cómo este producto ' +
    'ayuda a resolver, con ejemplos de código angular y enlaces para recursos';

  #model = this.#generativeAI.getGenerativeModel({
    model: 'gemini-pro',
  });
  
}

Ahora es el momento de generar el contenido usando el modelo. Primero, crea un método llamado generate con el parámetro textInput. Dado que el código de generateContent es asincrónico, la firma del método generate también debe ser asíncrona.

Primero, definimos las parts para configurar el contenido a generar, usando el prompt y el textInput del usuario. Esto ayuda a proporcionar contexto para el modelo. Luego, obtenemos el modelResult del modelo, que devuelve varias propiedades. En nuestro caso, la más importante es la response. Este objeto incluye la función text para obtener el valor en bruto. Para probar nuestro código, podemos imprimirlo en la consola usando console.log().

Usamos try catch para manejar cualquier error en la solicitud de generateContent.
 async generate(textInput: string) {
    try {
      if (text) {
        const parts = [
          {
            text: this.#prompt,
          },
          { text: textInput }
        ];

        const modelResult = await this.#model.generateContent({
          contents: [{ role: 'user', parts }],
        });
        const response = modelResult.response;
        const text = response.text();
         console.log(text)
      }
    } catch (e: any) {
      console.log(e);
    }
  }

Para utilizar nuestro servicio, sigue estos pasos: inyecta el GeminiClientService en el AppComponent.ts, añade un nuevo método llamado getAnswer, y luego llama al método generate desde gemeniService.

export class AppComponent {
  geminiService = inject(GeminiService)

  async getAnswer(text: any) {
    await this.geminiService.generate(text);
  }

}

En el marcado HTML, añade un input con una referencia de plantilla #inputText. En el botón, llama al método getAnswer.


<input #userInput><button (click)="getAnswer(userInput.value)">Generate</button>

Guarda los cambios y mira nuestro "Kendo IA power by Gemini"! 🎉😎

Bien, funciona, pero no se ve muy bien. Hagámoslo visualmente atractivo usando Kendo Conversational UI.

Usando Conversational UI

Para comenzar, necesitamos instalar Kendo UI para Angular Conversational UI para trabajar usando esquemas:


ng add @progress/kendo-angular-conversational-ui

Importa el ChatModule en la sección de imports del app.component.ts, ya que proporciona el componente kendo-chat.


@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule,   ChatModule],
  templateUrl: './app.component.html',
  styleUrl: './app.component.scss',
})
export class AppComponent {

Comencemos a hacer la magia en el gemini.service.ts, proporcionará signals para sincronizar la respuesta de Gemini con el kendo-chat del usuario y los mensajes.

Necesitamos declarar dos usuarios para el chat: uno será el kendoIA, y el otro será el usuario, como yo. Cada usuario debe tener un ID único. Para mostrar una imagen de usuario atractiva, podemos usar imágenes de activos.

Estoy usando crypto.randomUUID() para generar un id único.
readonly #kendoIA: User = {
    id: crypto.randomUUID(),
    name: 'Kendo UI',
    avatarUrl: './assets/kendo.png',
  };

  public readonly user: User = {
    id: crypto.randomUUID(),
    name: 'Dany',
    avatarUrl: './assets/dany.jpg',
  };

Declara una signal $messages con un array de Message, e inicialízalo con el mensaje inicial:

$messages = signal<Message[]>([{
          author: this.#kendoIA,
          timestamp: new Date(),
          text: '¡Hola! 👋 ¿cómo puedo ayudarte con Kendo?'
  }]);

El kendo-chat devuelve un SendMessageEvent, que proporciona toda la información sobre el mensaje desde la UI, como el usuario, mensaje y más. Actualizamos el método generate con la firma de SendMessage y actualizamos la lógica inicial leyendo el message.text.

async generate(textInput: SendMessageEvent) {
    try {
      if (textInput.message.text && this.#model) {

A continuación, actualiza la signals messages usando el método update, incorporando el nuevo mensaje y los componentes necesarios para leer el texto de textInput.message.text.

   this.$messages.update((p) => [...p, textInput.message]);
        const parts = [
          {
            text: this.#prompt,
          },
          { text: textInput.message.text },
        ];

Crea un nuevo mensaje con la respuesta del modelo, asigna el author: this.#kendoIA y actualiza nuevamente los mensajes.

const message = {
          author: this.#kendoIA,
          timestamp: new Date(),
          text,
        };

        this.$messages.update((p) => [...p, message]);

El código final se ve así:

async generate(textInput: SendMessageEvent) {
    try {
      if (textInput.message.text && this.#model) {
        this.$messages.update((p) => [...p, textInput.message]);
        const parts = [
          {
            text: this.#prompt,
          },
          { text: textInput.message.text },
        ];

        const result = await this.#model.generateContent({
          contents: [{ role: 'user', parts }],
        });
        
        const response = result.response;
        const text = response.text();

        const message = {
          author: this.#kendoIA,
          timestamp: new Date(),
          text,
        };

        this.$messages.update((p) => [...p, message]);
      }
    } catch (e: any) {
      console.log(e);
    }
  }

Conectar el Kendo Chat con Signals

¡Esta es la parte divertida de conectar con Signals! Utilizaremos el $messages y user del servicio Gemini. Primero, declara las variables en el app.component.ts.

export class AppComponent {

  geminiService = inject(GeminiService)
  user = this.geminiService.user;
  messages = this.geminiService.$messages

  async generate(event: SendMessageEvent) {
    await this.geminiService.generate(event);
  }
}

En la plantilla, añade el <kendo-chat> y vincula la propiedad user. Luego, lee los messages (con paréntesis) y vincula el sendMessage con el método generate previamente definido.

Ya que queremos mostrar tanto el avatar como el texto, modificamos la plantilla predeterminada usando la directiva kendoChatMessageTemplate para acceder a la variable message.

El marcado final se ve así:

  <kendo-chat [user]="user" [messages]="messages()" (sendMessage)="generate($event)" width="450px">
    <ng-template kendoChatMessageTemplate let-message>
      <div>
      {{ message.text }}
      {{message.avatar}}
      </div>
  </ng-template>

Guarda los cambios, ¡y voilà! Ahora tienes Gemini trabajando con conversational UI y una interfaz elegante!

Funciona pero después de mostrarle a @Jörgen de Groot mi demo de mi primer chat usando Gemini y el Conversational UI, preguntó cómo mantener el historial del chat y el contexto sin necesidad de enviar el prompt cada vez.

El Contexto y Tokens

El primer enfoque de enviar repetidamente el prompt incurre en un gasto y no admite mantener el contexto inicial o preservar el historial de la conversación.

Gemini ofrece una característica de chat que recopila nuestras preguntas y respuestas, permitiendo respuestas interactivas e incrementales dentro del mismo contexto. Esto es perfecto para nuestro Kendo ChatBot, así que implementemos estos cambios.

La Sesión de Chat

En nuestro primer enfoque jugamos con el modelo geminy para generar contenido. Esta vez, emplearemos el método startChat con el modelo para obtener un objeto ChatSession, que ofrece un historial y contexto inicial con el prompt.

El modelo Gemini ofrece una opción para iniciar una ChatSession, donde establecemos nuestro prompt inicial y conversación usando la característica startChat. El objeto ChatSession contiene un método sendMessage, que nos permite suministrar solo el segundo prompt del usuario.

Primero, declara un nuevo objeto chatSession con el historial inicial, que debe incluir el prompt inicial y la respuesta inicial, por ejemplo:

 #chatSession = this.#model.startChat({
    history: [
      {
        role: 'user',
        parts: this.#prompt,
      },
      {
        role: 'model',
        parts: "Sí, soy un experto en Angular con Kendo UI",
      },
    ],
    generationConfig: {
      maxOutputTokens: 100,
    },
  });

Nuestro siguiente paso es usar la chatSession en lugar de enviar directamente las partes y el rol user al model cada vez:

     const result = await this.#model.generateContent({
          contents: [{ role: 'user', parts }],
        });

Reemplaza el model con la chatSession y utiliza el método sendMessage:

 const result = await this.#chatSession.sendMessage(
          textInput.message.text,
       );

¡Hecho! 🎉 Nuestro chatbot ahora admite historial e interacción continua, sin enviar el prompt completo cada vez, ahorrando nuestros tokens 😊😁

👇

Resumen

Aprendimos cómo combinar el poder de Gemini con Angular e integrarlo con Angular y también demostramos cómo hacer que la interfaz sea visualmente atractiva usando el Angular Conversational UI.

Agregamos soporte de historial a nuestro chat, ahorrando tokens y mejorando la experiencia del usuario. Esto mejora la funcionalidad del Chatbot de Gemini manteniendo el historial y el contexto del chat, evitando la necesidad de reenviar prompts iniciales.

Usando la característica de chat, que recopila preguntas y respuestas, para respuestas interactivas e incrementales usando ChatSession y proporciona una mejor experiencia al usuario, y también ahorra tokens al no enviar el prompt completo cada vez. 💰🎉