✍️ Autor e Código-Fonte
Este artigo documenta um projeto real e de código aberto.
- Criado por: Ghabryel Henrique
- 🐙 GitHub: GhabryelHenrique
- 💼 LinkedIn: Conecte-se Comigo
Explore o código-fonte completo nos repositórios oficiais do projeto:
- 🔑 Para conseguir sua API Key do Gemini: AI Studio
- ⚙️ Aplicação Backend (NestJS): gemini-nest-engine
- 🖥️ Aplicação Frontend (Angular): build-with-AI-webapp-angular
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.
-
Criação do Projeto:
nest new gemini-nest-backend
-
Instalação do Módulo de Configuração:
npm install @nestjs/config
-
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
-
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.
-
Geração dos Recursos do Chat:
nest g module chat nest g controller chat nest g service chat
-
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.
-
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; }
-
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.
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.
Introduzindo o
sessionId
: Para distinguir as conversas de diferentes usuários, introduzimos o conceito desessionId
, um identificador único para cada chat.-
Refatoração do Serviço para Conversa: Modificamos nosso
ChatService
para manter umMap
, onde a chave era osessionId
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:
- 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. - 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.
-
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 {}
-
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();
}
}
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:
- Identidade e Papel: Quem é você? (Ex: "Você é Alex, um consultor de vendas sênior").
- Missão e Objetivos: Qual é o seu propósito principal? (Ex: "Seu objetivo é entender as dores do cliente e agendar uma demonstração").
- Tom de Voz e Personalidade: Como você deve soar? (Ex: "Profissional, consultivo, proativo, nunca 'vendedor'").
- Conhecimento Específico: Qual é a sua base de conhecimento? (Ex: "Você conhece os módulos Financeiro, Estoque e Vendas do Nexus ERP...").
- Limites e Restrições: O que você NUNCA deve fazer? (Ex: "Você nunca discute preços; para isso, direcione para uma demonstração").
- 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.
-
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. `;
-
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étodorun
)
// ... 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.
-
Criação do Projeto:
ng new gemini-angular-chat --standalone
-
Configurando os Provedores Globais: Em aplicações standalone, a configuração de serviços globais como o
HttpClient
acontece no arquivoapp.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 ] };
-
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);
}
}
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.
-
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' }); } }); } }
-
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>
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.
-
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()), ] };
-
Implementação: Adicionamos o
MarkdownModule
aosimports
do nossoChatComponent
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.
-
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
-
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);
-
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.
Injeção de Dependência: O AgentService (do chat) passou a injetar o novo
AgentsService
, ganhando acesso ao nosso banco de dados de agentes.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 doagentsService.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.
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.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.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.Usando Bootstrap, criamos uma grade de cards responsivos.
Cada card representava um agente, mostrando seu nome e um trecho da sua persona.
-
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)