DEV Community

Cover image for Do Zero à Plataforma de IA Multi-Agente: Um Guia Completo com Gemini, NestJS e Angular

Do Zero à Plataforma de IA Multi-Agente: Um Guia Completo com Gemini, NestJS e Angular

✍️ Autor e Código-Fonte

Este artigo documenta um projeto real e de código aberto.

Explore o código-fonte completo nos repositórios oficiais do projeto:


A era da Inteligência Artificial generativa está revolucionando a forma como interagimos com a tecnologia. Mas como podemos ir além de simples chatbots e construir aplicações complexas, personalizadas e verdadeiramente úteis? Este guia é o diário de bordo de uma jornada de desenvolvimento, documentando a criação de uma sofisticada plataforma de chat multi-persona, do zero até o deploy.

Nesta série, vamos explorar cada passo da construção de uma aplicação full-stack, utilizando o poder da API do Google Gemini, a estrutura robusta do NestJS para o backend, e a reatividade do Angular para o frontend.

A Visão: Uma Plataforma, Múltiplas Inteligências

Nosso objetivo era ambicioso: não apenas um agente de IA, mas uma plataforma onde diferentes "especialistas" pudessem ser criados, customizados e utilizados sob demanda. O resultado é uma aplicação onde um usuário pode:

  • Criar Agentes: Definir novos agentes com nomes e "personas" (instruções e personalidades) únicas.
  • Selecionar um Especialista: Escolher com qual agente deseja conversar antes de iniciar o chat.
  • Manter o Contexto: Ter conversas fluidas com histórico persistente, armazenado em um banco de dados.
  • Obter Respostas Ricas: Receber respostas formatadas em Markdown, com listas, código e ênfase.

Parte 1: A Fundação — Backend com NestJS e a Primeira Conexão

Toda grande construção começa com uma base sólida. Para o nosso backend, a escolha foi o NestJS, um framework Node.js que impõe uma arquitetura modular e escalável. Seu uso nativo de TypeScript e seu sistema de Injeção de Dependência o tornam a escolha perfeita para projetos complexos e para manter a sinergia com o frontend em Angular.

1.1. Configurando o Projeto

Iniciamos com a CLI do NestJS e, imediatamente, implementamos uma prática essencial: o gerenciamento de configurações com variáveis de ambiente.

  1. Criação do Projeto:

    nest new gemini-nest-backend
    
  2. Instalação do Módulo de Configuração:

    npm install @nestjs/config
    
  3. Criação do Arquivo .env: Na raiz do projeto, criamos o arquivo para nossas chaves secretas.

    # .env
    GEMINI_API_KEY=SUA_CHAVE_SECRETA_DO_GOOGLE_AI_AQUI
    
  4. Configuração do AppModule: Informamos ao NestJS para carregar as variáveis do .env e torná-las disponíveis globalmente.

    Arquivo: src/app.module.ts

    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config';
    import { ChatModule } from './chat/chat.module'; // Nosso futuro módulo de chat
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true, // Torna as variáveis de ambiente globais
        }),
        ChatModule,
      ],
    })
    export class AppModule {}
    

1.2. A Primeira Chamada à API: O "Olá, Mundo!" da IA

O primeiro objetivo era validar a conexão com a API do Gemini. Para isso, criamos um serviço simples que continha a lógica de comunicação.

  1. Geração dos Recursos do Chat:

    nest g module chat
    nest g controller chat
    nest g service chat
    
  2. Lógica Inicial do Serviço: O ChatService foi o coração da nossa lógica. No início, ele continha apenas um método para enviar um prompt e receber uma resposta de volta.

    Arquivo: src/chat/chat.service.ts (Versão Inicial)

    import { Injectable, OnModuleInit } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import { GoogleGenerativeAI, GenerativeModel } from '@google/generative-ai';
    
    @Injectable()
    export class ChatService implements OnModuleInit {
      private generativeModel: GenerativeModel;
    
      constructor(private configService: ConfigService) {}
    
      // onModuleInit é um hook do NestJS que roda quando o módulo é inicializado
      onModuleInit() {
        const apiKey = this.configService.get<string>('GEMINI_API_KEY');
        const genAI = new GoogleGenerativeAI(apiKey);
        this.generativeModel = genAI.getGenerativeModel({
          model: 'gemini-2.5-flash',
        });
      }
    
      /**
       * Envia um prompt simples e de turno único para o Gemini.
       * @param prompt O texto a ser enviado.
       * @returns A resposta de texto gerada pelo modelo.
       */
      async simplePrompt(prompt: string): Promise<string> {
        try {
          const result = await this.generativeModel.generateContent(prompt);
          const response = result.response;
          return response.text();
        } catch (error) {
          console.error('Erro ao chamar a API do Gemini:', error);
          throw new Error('Falha ao se comunicar com o serviço de IA.');
        }
      }
    }
    

1.3. Criando o Primeiro Endpoint da API

Com o serviço funcionando, precisávamos de uma porta de entrada para o mundo exterior: um endpoint HTTP. O ChatController cumpriu esse papel.

  1. Criação do DTO (Data Transfer Object): Para garantir que os dados recebidos tivessem o formato correto, criamos um DTO.

    Arquivo: src/chat/dto/prompt.dto.ts

    export class PromptDto {
      prompt: string;
    }
    
  2. Implementação do Controller: O controller define a rota (/chat) e o método HTTP (POST), recebe os dados do corpo da requisição e chama o serviço para processá-los.

    Arquivo: src/chat/chat.controller.ts (Versão Inicial)

    import { Body, Controller, Post } from '@nestjs/common';
    import { ChatService } from './chat.service';
    import { PromptDto } from './dto/prompt.dto';
    
    @Controller('chat')
    export class ChatController {
      constructor(private readonly chatService: ChatService) {}
    
      @Post('simple')
      async handleSimplePrompt(@Body() promptDto: PromptDto) {
        const response = await this.chatService.simplePrompt(promptDto.prompt);
        return { response };
      }
    }
    

Com essa base, tínhamos um backend funcional, seguro e pronto para evoluir. Ele já podia receber uma pergunta, consultar a IA mais poderosa da Google e retornar uma resposta inteligente. Mas isso era apenas o começo. A próxima etapa seria dar a esse agente a capacidade de se lembrar, de conversar.


Pausa para a publi

🚨 Deu a Louca no Gerente! 🚨

A StackSpot AI agora é FREEMIUM — com tudo liberado!

Sim, você leu certo:
✅ Todas as funcionalidades disponíveis
✅ Sem prazo de expiração
✅ 2 MILHÕES de tokens mensais GRÁTIS com o meu link 👇
👉 https://ai.stackspot.com/?campaignCode=01JXZTRDP72NTCSC6JD6NAXKZN

Se você é da área de desenvolvimento ou atua no negócio, essa plataforma foi feita pra você:
✔️ Intuitiva
✔️ Escalável
✔️ Ideal para criar soluções com mais autonomia, menos burocracia e o suporte da IA

💥 Teste agora e veja como a StackSpot pode revolucionar sua produtividade.

Imagem com fundo preto pedindo para você usar meu link


Na Parte 1, construímos uma base sólida: um backend NestJS capaz de fazer requisições simples à API do Gemini. No entanto, uma IA que não se lembra da pergunta anterior não pode verdadeiramente "conversar". Ela é apenas um oráculo de pergunta e resposta. Nesta segunda parte, vamos dar o passo crucial para transformar nosso oráculo em um verdadeiro conversador, implementando a memória e, em seguida, tornando-a permanente com um banco de dados.

2.1. A Memória de Curto Prazo: Gerenciando Sessões de Chat

Para que o Gemini "se lembre" do que foi dito, precisamos reenviar todo o histórico da conversa a cada nova mensagem. A biblioteca do Gemini facilita isso com o método startChat. Nossa primeira abordagem foi gerenciar essas sessões de chat na memória do servidor.

  1. Introduzindo o sessionId: Para distinguir as conversas de diferentes usuários, introduzimos o conceito de sessionId, um identificador único para cada chat.

  2. Refatoração do Serviço para Conversa: Modificamos nosso ChatService para manter um Map, onde a chave era o sessionId e o valor era o objeto da sessão de chat ativa.

    Arquivo: src/chat/chat.service.ts (Versão com Memória em RAM)

    import { ChatSession, ... } from '@google/generative-ai';
    // ...
    
    @Injectable()
    export class ChatService implements OnModuleInit {
      private generativeModel: GenerativeModel;
      // Um Map para guardar as sessões de chat ativas na memória do servidor.
      private chatSessions = new Map<string, ChatSession>();
    
      constructor(private configService: ConfigService) {}
    
      // ... onModuleInit continua o mesmo ...
    
      /**
       * Gerencia uma conversa contínua com o Gemini.
       */
      async conversationalRun(prompt: string, sessionId: string): Promise<string> {
        // 1. Procura por uma sessão de chat existente no nosso Map.
        let chat = this.chatSessions.get(sessionId);
    
        // 2. Se não existir, cria uma nova e a armazena.
        if (!chat) {
          console.log(`Iniciando nova sessão de chat para o ID: ${sessionId}`);
          chat = this.generativeModel.startChat({
            history: [], // Começa com histórico vazio
          });
          this.chatSessions.set(sessionId, chat);
        }
    
        // 3. Envia a nova mensagem dentro do contexto da sessão.
        const result = await chat.sendMessage(prompt);
        const response = result.response;
        return response.text();
      }
    }
    

Isso funcionou! Agora podíamos ter múltiplas conversas simultâneas e o agente se lembrava do contexto de cada uma. Porém, essa solução tinha um ponto fraco fatal.

2.2. A Necessidade de Persistência: A Fragilidade da Memória RAM

Armazenar as sessões na memória RAM é rápido e simples para prototipagem, mas inviável para uma aplicação real por duas razões críticas:

  1. Volatilidade: Se o servidor fosse reiniciado por qualquer motivo (uma atualização, um erro, etc.), o Map chatSessions seria completamente apagado. Todas as conversas em andamento seriam perdidas para sempre.
  2. Escalabilidade: Em um ambiente de produção, é comum ter múltiplas instâncias do servidor rodando em paralelo para lidar com o tráfego (load balancing). Um usuário poderia enviar a primeira mensagem para o Servidor A e a segunda para o Servidor B. O Servidor B não teria ideia da conversa iniciada no Servidor A, quebrando a continuidade.

A solução para ambos os problemas é a mesma: um banco de dados externo e compartilhado.

2.3. A Memória de Longo Prazo: Integração com MongoDB

Escolhemos o MongoDB, um banco de dados NoSQL, por sua flexibilidade em armazenar documentos complexos como nosso histórico de conversa. Usamos o Mongoose como ODM (Object Data Modeling) para facilitar a interação no NestJS.

  1. Instalação e Conexão:

    npm install @nestjs/mongoose mongoose
    

    Configuramos a conexão no app.module.ts, buscando a URL do banco de dados de forma segura do nosso arquivo .env.

    Arquivo: src/app.module.ts (Adicionando Mongoose)

    // ...
    import { MongooseModule } from '@nestjs/mongoose';
    
    @Module({
      imports: [
        ConfigModule.forRoot({ isGlobal: true }),
        MongooseModule.forRootAsync({
          imports: [ConfigModule],
          useFactory: async (configService: ConfigService) => ({
            uri: configService.get<string>('DATABASE_URL'), // Ex: mongodb://localhost:27017/chat-db
          }),
          inject: [ConfigService],
        }),
        // ...
      ],
    })
    export class AppModule {}
    
  2. Modelando a Conversa: Definimos a "forma" dos nossos dados no banco de dados criando um Schema.

    Arquivo: src/chat/schemas/conversation.schema.ts

    import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
    import { Document, HydratedDocument } from 'mongoose';
    import { Content } from '@google/generative-ai';
    
    export type ConversationDocument = HydratedDocument<Conversation>;
    
    @Schema({ timestamps: true }) // Adiciona os campos createdAt e updatedAt
    export class Conversation {
      @Prop({ required: true, unique: true, index: true })
      sessionId: string;
    
      @Prop({ type: Array, required: true })
      history: Content[]; // O histórico completo da conversa
    }
    
    export const ConversationSchema = SchemaFactory.createForClass(Conversation);
    

2.4. Refatoração Final: O Serviço de Chat Persistente

Com o banco de dados conectado e o modelo definido, refatoramos o ChatService pela última vez para alcançar a persistência.

Arquivo: src/chat/chat.service.ts (Versão Final com Persistência)

import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { GoogleGenerativeAI, GenerativeModel, Content } from '@google/generative-ai';
import { Conversation, ConversationDocument } from './schemas/conversation.schema';

@Injectable()
export class ChatService implements OnModuleInit {
  private generativeModel: GenerativeModel;

  constructor(
    private configService: ConfigService,
    // Injetamos o modelo do Mongoose para interagir com a coleção 'conversations'
    @InjectModel(Conversation.name)
    private conversationModel: Model<ConversationDocument>,
  ) {}

  // ... onModuleInit continua o mesmo ...

  async run(prompt: string, sessionId: string): Promise<string> {
    // 1. Busca o histórico da conversa no MongoDB usando o sessionId.
    const conversation = await this.conversationModel.findOne({ sessionId }).exec();
    const history: Content[] = conversation ? conversation.history : [];

    // 2. Inicia a sessão de chat com o Gemini, usando o histórico recuperado.
    const chat = this.generativeModel.startChat({ history });

    // 3. Envia a nova mensagem do usuário.
    const result = await chat.sendMessage(prompt);

    // 4. Obtém o histórico ATUALIZADO, que agora inclui o último prompt e a última resposta.
    const updatedHistory = await chat.getHistory();

    // 5. Salva o histórico completo de volta no MongoDB.
    // O 'upsert: true' cria um novo documento se nenhum for encontrado com o sessionId.
    await this.conversationModel.findOneAndUpdate(
      { sessionId },
      { history: updatedHistory },
      { upsert: true, new: true },
    ).exec();

    const response = result.response;
    return response.text();
  }
}
Enter fullscreen mode Exit fullscreen mode

Com esta arquitetura, nosso agente agora tinha uma memória perfeita e duradoura. Cada conversa podia ser pausada e retomada a qualquer momento, e a aplicação estava pronta para ser escalada sem medo de perder dados. O agente podia se lembrar.


Nesta terceira parte, daremos o salto quântico: transformaremos nosso chatbot genérico em um especialista focado. Vamos explorar como injetar um "hiper contexto" — uma Persona — para guiar cada ação e palavra do nosso agente.

3.1. A Anatomia de uma Persona de IA

Para instruir um Modelo de Linguagem Grande (LLM) como o Gemini, a abordagem mais eficaz é o "prompt de sistema" ou, como chamamos, a definição da Persona. Trata-se de um conjunto detalhado de instruções fornecido no início da conversa, que funciona como o "manual de instruções" do agente para toda a sessão.

Uma persona bem construída é a diferença entre um brinquedo e uma ferramenta de negócios. Ela deve conter:

  1. Identidade e Papel: Quem é você? (Ex: "Você é Alex, um consultor de vendas sênior").
  2. Missão e Objetivos: Qual é o seu propósito principal? (Ex: "Seu objetivo é entender as dores do cliente e agendar uma demonstração").
  3. Tom de Voz e Personalidade: Como você deve soar? (Ex: "Profissional, consultivo, proativo, nunca 'vendedor'").
  4. Conhecimento Específico: Qual é a sua base de conhecimento? (Ex: "Você conhece os módulos Financeiro, Estoque e Vendas do Nexus ERP...").
  5. Limites e Restrições: O que você NUNCA deve fazer? (Ex: "Você nunca discute preços; para isso, direcione para uma demonstração").
  6. Formato da Resposta: Como você deve estruturar suas respostas? (Ex: "Use formatação Markdown para clareza, com listas e negrito").

3.2. Implementando a Persona no Backend

Com a arquitetura da persona definida, a implementação no NestJS foi surpreendentemente direta.

  1. Criando o Arquivo da Persona: Para manter o código organizado, criamos um arquivo separado para nossa persona detalhada.

    Arquivo: src/utils/persona.ts

    export const ERP_SALES_PERSONA = `
    Você é o Alex, um especialista sênior em soluções empresariais e consultor de vendas de um sistema de gestão ERP de ponta chamado "Nexus ERP". Você não é um chatbot genérico, você é um profissional experiente.
    
    **Sua Missão:**
    Seu objetivo principal é entender as dores e necessidades do negócio do cliente e apresentar as funcionalidades do Nexus ERP como a solução ideal. Você deve guiar a conversa, fazer perguntas inteligentes e, finalmente, agendar uma demonstração.
    
    **Seu Tom de Voz e Personalidade:**
    - Profissional e Confiante: Você sabe do que está falando.
    - Consultivo, não "vendedor": Seu objetivo é ajudar, não forçar uma venda.
    - Focado: Nunca saia do seu papel. Se perguntarem sobre o tempo, gentilmente redirecione a conversa para os desafios de negócio.
    
    **Formato da Resposta:**
    Sempre que possível, use formatação Markdown para melhorar a clareza. Use listas numeradas, texto em **negrito** e blocos de código quando apropriado.
    
    **Limites:**
    Você nunca discute preços. Para isso, você deve direcionar o cliente para o agendamento de uma demonstração.
    `;
    
  2. Injetando a Persona no Início do Chat: A mágica acontece ao modificar como iniciamos uma nova conversa. Em vez de começar com um histórico vazio, nós o "preparamos" com a persona.

    Arquivo: src/chat/chat.service.ts (Modificação no método run)

    // ...
    import { ERP_SALES_PERSONA } from 'src/utils/persona';
    // ...
    
    async run(prompt: string, sessionId: string): Promise<string> {
      const conversation = await this.conversationModel.findOne({ sessionId }).exec();
      let history: Content[] = conversation ? conversation.history : [];
    
      // AQUI ESTÁ A MUDANÇA CRUCIAL
      // Se a conversa é nova (histórico vazio), injetamos a persona.
      if (history.length === 0) {
        history = [
          {
            // Simulamos o "usuário" (nós, desenvolvedores) dando a instrução.
            role: 'user',
            parts: [{ text: ERP_SALES_PERSONA }],
          },
          {
            // Simulamos o "modelo" aceitando seu novo papel. Isso o prepara para a conversa.
            role: 'model',
            parts: [{ text: 'Entendido. Eu sou Alex, especialista em soluções do Nexus ERP. Estou pronto para ajudar.' }],
          },
        ];
      }
    
      // O restante do fluxo continua o mesmo...
      const chat = this.generativeModel.startChat({ history });
      const result = await chat.sendMessage(prompt);
      const updatedHistory = await chat.getHistory();
    
      // ... (lógica de salvar no banco de dados) ...
    
      return result.response.text();
    }
    

3.3. O Resultado: Um Especialista Sob Demanda

O impacto dessa mudança foi imediato e profundo.

Prompt do usuário: "Olá, tudo bem?"

  • Resposta ANTES da Persona:

    "Olá! Tudo bem, e com você? Como posso te ajudar hoje?"
    (Genérico e reativo)

  • Resposta DEPOIS da Persona:

    "Olá! Tudo ótimo por aqui. Sou Alex, consultor de soluções da Nexus ERP. Para que eu possa entender melhor como te ajudar, poderia me contar um pouco sobre sua empresa e os principais desafios que você enfrenta na sua gestão hoje?"
    (Focado, proativo e alinhado com o objetivo de negócio)

Nosso agente deixou de ser um passageiro na conversa para se tornar o motorista.

3.4. Uma Nota Sobre o Próximo Nível: Function Calling

Embora nosso projeto final tenha focado em personas dinâmicas, uma etapa crucial na exploração dos recursos do Gemini foi o Function Calling. Esta é a capacidade do Gemini de, em vez de apenas responder, instruir nosso backend a executar uma função local — como buscar o preço de um produto em tempo real ou verificar a disponibilidade de estoque em outro sistema — e depois usar o resultado dessa função para construir a resposta final.

Este conceito transforma o agente de um "conhecedor" para um "fazedor", permitindo integrações em tempo real com qualquer outra API ou fonte de dados. É uma ferramenta avançada e poderosa no arsenal do desenvolvedor de agentes.


Nas partes anteriores, forjamos um backend robusto no NestJS. Nosso agente agora possui uma memória persistente com MongoDB e uma personalidade definida através de um "hiper contexto". No entanto, essa inteligência permanece trancada em nosso servidor. Nesta quarta parte, construiremos a janela para este mundo: uma interface de usuário reativa e elegante com Angular.

Sua arquitetura baseada em componentes, tipagem forte com TypeScript e ecossistema robusto o tornam o parceiro ideal para nosso backend NestJS, criando uma experiência de desenvolvimento full-stack coesa e poderosa.

4.1. Configuração Inicial e a Ponte Entre Mundos (CORS)

Adotamos a abordagem moderna do Angular com componentes standalone, eliminando a necessidade de NgModules para cada feature e simplificando a arquitetura.

  1. Criação do Projeto:

    ng new gemini-angular-chat --standalone
    
  2. Configurando os Provedores Globais: Em aplicações standalone, a configuração de serviços globais como o HttpClient acontece no arquivo app.config.ts.

    Arquivo: src/app/config.ts

    import { ApplicationConfig } from '@angular/core';
    import { provideRouter } from '@angular/router';
    import { provideHttpClient } from '@angular/common/http'; // A forma moderna de fornecer o HttpClient
    
    import { routes } from './app.routes';
    
    export const appConfig: ApplicationConfig = {
      providers: [
        provideRouter(routes),
        provideHttpClient(), // Garante que o HttpClient esteja disponível em toda a aplicação
      ]
    };
    
  3. A Ponte Essencial - CORS: Antes de qualquer requisição, precisávamos instruir nosso backend a confiar no nosso frontend. Sem isso, todas as chamadas seriam bloqueadas pelo navegador. Relembrando o ajuste crucial no backend NestJS:

    Arquivo: src/main.ts (no NestJS)

    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      app.enableCors({
        origin: 'http://localhost:4200', // Permite requisições da nossa app Angular
      });
      await app.listen(3000);
    }
    bootstrap();
    

4.2. O Mensageiro: Criando o ChatService

Em Angular, os Serviços são a espinha dorsal da comunicação com APIs externas. Eles encapsulam a lógica HTTP, mantendo nossos componentes limpos e focados na UI.

Arquivo: src/app/services/chat.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

// Definindo interfaces para segurança de tipo
export interface ApiResponse {
  response: string;
  sessionId: string;
}

@Injectable({
  providedIn: 'root'
})
export class ChatService {
  private readonly apiUrl = 'http://localhost:3000/chat'; // URL do nosso backend

  constructor(private http: HttpClient) { }

  // Na fase inicial, o método envia o prompt e o sessionId opcional
  sendMessage(prompt: string, sessionId?: string | null): Observable<ApiResponse> {
    const body = { prompt, sessionId };
    return this.http.post<ApiResponse>(this.apiUrl, body);
  }
}
Enter fullscreen mode Exit fullscreen mode

4.3. O Palco Principal: Construindo o ChatComponent

Este componente é o coração da nossa interface. Ele gerencia o estado da conversa, interage com o ChatService e renderiza as mensagens.

  1. A Lógica (chat.component.ts):

    import { Component } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { FormsModule } from '@angular/forms'; // Necessário para [(ngModel)]
    import { Message } from 'src/app/models/chat.model'; // Nossa interface
    import { ChatService } from 'src/app/services/chat.service';
    
    @Component({
      selector: 'app-chat',
      standalone: true,
      imports: [CommonModule, FormsModule], // Importa o que o template precisa
      templateUrl: './chat.component.html',
      styleUrls: ['./chat.component.css']
    })
    export class ChatComponent {
      messages: Message[] = [];
      newMessage: string = '';
      sessionId: string | null = null;
      isLoading: boolean = false;
    
      constructor(private chatService: ChatService) {}
    
      sendMessage(): void {
        if (!this.newMessage.trim()) return;
    
        // Adiciona a mensagem do usuário à UI imediatamente para uma resposta visual rápida
        this.messages.push({ text: this.newMessage, sender: 'user' });
        const userPrompt = this.newMessage;
        this.newMessage = '';
        this.isLoading = true; // Ativa o indicador de "digitando..."
    
        // Chama o serviço e se inscreve na resposta
        this.chatService.sendMessage(userPrompt, this.sessionId).subscribe({
          next: (apiResponse) => {
            this.isLoading = false;
            this.messages.push({ text: apiResponse.response, sender: 'bot' });
            this.sessionId = apiResponse.sessionId; // Armazena o ID da sessão para continuidade
          },
          error: (err) => {
            this.isLoading = false;
            this.messages.push({ text: 'Desculpe, ocorreu um erro.', sender: 'bot' });
          }
        });
      }
    }
    
  2. O Template (chat.component.html): Usamos diretivas do Angular para criar uma interface dinâmica.

    O Template (chat.component.html): Usamos diretivas do Angular para criar uma interface dinâmica.

<section class="container">
  <div class="agent-selector-container text-center" *ngIf="!selectedAgentId">
    <h2 class="title mb-3">Olá! Como posso ajudar hoje?</h2>
    <h4 class="subtitle mb-5">Selecione um agente para começar</h4>

    <div class="row g-4 justify-content-center">
      <div class="col-xl-3 col-md-4 col-sm-6" *ngFor="let agent of agents">
        <div
          class="card agent-card h-100"
          (click)="selectAgent(agent._id)"
          [class.selected]="agent._id === selectedAgentId"
        >
          <div class="card-body">
            <h5 class="card-title">{{ agent.name }}</h5>
            <p class="card-text">{{ agent.persona.substring(0, 80) }}...</p>
          </div>
        </div>
      </div>
    </div>
  </div>
<div
    *ngIf="selectedAgentId || agents.length === 0"
    class="d-flex flex-column justify-content-end h-100"
  >
    <div class="d-flex flex-column justify-content-end h-100">
      <div class="chat-messages mb-3">
        <div
          *ngFor="let message of messages"
          class="message-wrapper"
          [class.justify-content-end]="message.sender === 'user'"
        >
          <div
            class="card p-3"
            [ngClass]="{ responseAgent: message.sender === 'bot' }"
          >
            <markdown [data]="message.text" class="m-0"></markdown>
          </div>
        </div>

        <div *ngIf="isLoading" class="message-wrapper">
          <div class="card responseAgent p-3">
            <p class="m-0 typing-indicator">
              {{ agentName }}
              está digitando<span>.</span><span>.</span><span>.</span>
            </p>
          </div>
        </div>
      </div>

      <form (ngSubmit)="sendMessage()">
        <input
          type="text"
          class="input-prompt"
          name="newMessage"
          placeholder="Digite sua mensagem..."
          autocomplete="off"
          [disabled]="isLoading"
          [(ngModel)]="newMessage"
        />
        <button type="submit" style="display: none"></button>
      </form>
    </div>
  </div>
</section>

Enter fullscreen mode Exit fullscreen mode

4.4. Enriquecendo a Conversa com Markdown

Para que nosso agente pudesse responder com listas, código e outros formatos, precisávamos de uma forma de renderizar Markdown no frontend. A biblioteca ngx-markdown foi a escolha perfeita.

  1. Instalação e Configuração:

    npm install ngx-markdown
    

    Configuramos o provider no app.config.ts, como vimos anteriormente, para disponibilizar os recursos da biblioteca para a aplicação.

    Arquivo: src/app/config.ts

    // ...
    import { importProvidersFrom } from '@angular/core';
    import { MarkdownModule } from 'ngx-markdown';
    
    export const appConfig: ApplicationConfig = {
      providers: [
        // ... outros providers
        importProvidersFrom(MarkdownModule.forRoot()),
      ]
    };
    
  2. Implementação: Adicionamos o MarkdownModule aos imports do nosso ChatComponent standalone e substituímos a exibição de texto simples pela diretiva da biblioteca.

    Arquivo: src/app/components/chat/chat.component.html (A Mudança)

    <markdown [data]="message.text" class="m-0"></markdown>
    

    Adicionamos também CSS para estilizar os elementos HTML (<pre>, <code>, <ul>) gerados pelo Markdown, garantindo uma aparência limpa e profissional para os blocos de código e listas.

Com a interface construída, tínhamos um canal de comunicação direto e elegante com nosso agente especialista. O usuário podia conversar, ver respostas ricas e ter uma experiência fluida. Estávamos com uma aplicação de IA impressionante em mãos. Mas ainda faltava o grande final.

Nas quatro partes anteriores, construímos meticulosamente os pilares da nossa aplicação. Temos um backend NestJS com um agente de memória persistente, uma personalidade definida e um frontend Angular reativo para conversar com ele. Agora, na parte final, vamos executar a visão mais ambiciosa: transformar nossa aplicação de um único especialista em uma plataforma dinâmica e multi-agente, onde o próprio usuário pode criar, gerenciar e escolher com qual persona de IA deseja interagir.

Esta é a etapa que eleva o projeto de uma ferramenta impressionante para um produto verdadeiramente flexível e poderoso.

5.1. A Expansão do Backend: Nasce o Gerenciador de Agentes

Para permitir agentes dinâmicos, nosso backend precisava evoluir. A lógica de chat não era mais suficiente; precisávamos de um sistema completo de CRUD (Create, Read, Update, Delete) para gerenciar as personas.

  1. Nova Estrutura Modular: Criamos um módulo totalmente novo, AgentsModule, com seu próprio controller, serviço e schema, seguindo as melhores práticas do NestJS.

    nest g module agents
    nest g controller agents
    nest g service agents
    
  2. O Schema do Agente: Definimos a estrutura de um agente no MongoDB. Simples, mas poderosa: um nome para identificação e uma persona para as instruções.

    Arquivo: src/agents/schemas/agent.schema.ts

    import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
    
    @Schema({ timestamps: true })
    export class Agent {
      @Prop({ required: true, unique: true })
      name: string;
    
      @Prop({ required: true, type: String })
      persona: string;
    }
    
    export const AgentSchema = SchemaFactory.createForClass(Agent);
    
  3. API de Gerenciamento: Implementamos os endpoints essenciais no AgentsController para permitir que nosso futuro frontend pudesse criar e listar agentes.

    Arquivo: src/agents/agents.controller.ts

    import { Body, Controller, Get, Param, Post } from '@nestjs/common';
    import { AgentsService } from './agents.service';
    import { CreateAgentDto } from './dto/create-agent.dto';
    
    @Controller('agents')
    export class AgentsController {
      constructor(private readonly agentsService: AgentsService) {}
    
      @Post()
      create(@Body() createAgentDto: CreateAgentDto) {
        return this.agentsService.create(createAgentDto);
      }
    
      @Get()
      findAll() {
        return this.agentsService.findAll();
      }
    }
    

5.2. A Conexão Crítica: Adaptando o Chat para Ser Dinâmico

Com a capacidade de armazenar múltiplos agentes, o passo final no backend foi refatorar nosso serviço de chat original (AgentService) para que ele pudesse usar essas novas personas dinâmicas.

  1. Injeção de Dependência: O AgentService (do chat) passou a injetar o novo AgentsService, ganhando acesso ao nosso banco de dados de agentes.

  2. A Lógica Dinâmica: O método run foi aprimorado para receber um agentId do frontend. A mágica acontecia no início de uma nova conversa:

  • Ele não usava mais uma persona fixa.

  • Em vez disso, usava o agentId para buscar a persona correspondente no banco de dados através do agentsService.findOne(agentId).

  • Essa persona recuperada era então injetada como o "hiper contexto" da conversa.

    Arquivo: src/agent/agent.service.ts (A lógica central)

    // ...
    async run(prompt: string, sessionId: string, agentId: string): Promise<string> {
      const conversation = await this.conversationModel.findOne({ sessionId }).exec();
      let history: Content[] = conversation ? conversation.history : [];
    
      if (history.length === 0) {
        // 1. Busca o agente específico no banco de dados.
        const agent = await this.agentsService.findOne(agentId);
        if (!agent) {
          throw new NotFoundException(`Agente com ID "${agentId}" não encontrado.`);
        }
    
        // 2. Usa a persona DESSE agente para iniciar o chat.
        history = [
          { role: 'user', parts: [{ text: agent.persona }] },
          { role: 'model', parts: [{ text: 'Entendido. Estou pronto para começar.' }] },
        ];
      }
      // ... o resto do fluxo de chat continua como antes
    }
    

5.3. A Interface da Plataforma no Angular

O backend estava pronto para ser multi-agente. Agora, precisávamos de uma interface que permitisse ao usuário exercer esse poder.

  1. Serviço e Rotas de Gerenciamento: Criamos um AgentService no Angular para se comunicar com os novos endpoints /agents e configuramos rotas para as páginas de listagem e criação.

  2. Formulário de Criação: Usando ReactiveFormsModule, construímos uma página onde o usuário podia facilmente digitar o nome e a persona de um novo agente e salvá-lo no banco de dados.

  3. A Tela de Seleção: Este foi o auge da nossa UX. Inspirados pela interface do próprio Gemini, abandonamos um simples <select> e criamos uma tela de boas-vindas visual e interativa.

  4. Usando Bootstrap, criamos uma grade de cards responsivos.

  5. Cada card representava um agente, mostrando seu nome e um trecho da sua persona.

  6. Um evento (click) em um card selecionava o agente, e uma classe CSS dinâmica ([class.selected]) o destacava visualmente.

    Arquivo: src/app/components/chat/chat.component.html (A seleção)

    <div class="agent-selector-container text-center" *ngIf="messages.length === 0">
      <h2 class="title mb-3">Olá! Como posso ajudar hoje?</h2>
      <h4 class="subtitle mb-5">Selecione um agente para começar</h4>
    
      <div class="row g-4 justify-content-center">
        <div class="col-xl-3 col-md-4 col-sm-6" *ngFor="let agent of agents">
          <div class="card agent-card h-100"
               (click)="selectAgent(agent._id)"
               [class.selected]="agent._id === selectedAgentId">
            <div class="card-body">
              <h5 class="card-title">{{ agent.name }}</h5>
            </div>
          </div>
        </div>
      </div>
    </div>
    
    <div *ngIf="selectedAgentId" class="chat-container-active">
      </div>
    

A interface de chat só era renderizada após a seleção, garantindo que cada conversa começasse com o agentId correto, pronto para ser enviado ao backend.

Conclusão Final: Da Ideia à Plataforma

Nossa jornada nos levou muito além de uma simples chamada de API. Ao combinar a arquitetura disciplinada do NestJS, a reatividade do Angular e a flexibilidade do MongoDB, transformamos a poderosa IA do Google Gemini em uma plataforma viva, respirável e, acima de tudo, personalizável.

Construímos um sistema onde a inteligência não é monolítica, mas sim um conjunto de especialistas que podem ser criados e invocados dinamicamente. Passamos por todas as etapas cruciais do desenvolvimento de software moderno: configuração segura, arquitetura modular, persistência de dados, design de API, gerenciamento de estado no frontend e refinamento da experiência do usuário.

O resultado final é um testemunho do que é possível quando combinamos as ferramentas certas com uma visão clara: uma aplicação robusta, escalável e pronta para moldar a próxima geração de interações humano-computador.

Top comments (0)