Publicado 28 de fevereiro de 2024
Vamos analisar o código passo a passo para entender como ele funciona e o que cada parte faz.
O projeto está estruturado da seguinte forma:
1├── ChatBot.sln
2├── compose.yaml
3└── src
4 ├── API
5 │ ├── Dockerfile
6 │ ├── Extensions
7 │ │ ├── BuilderExtensions.cs
8 │ │ └── GoogleCloudLanguageApiHealthCheck.cs
9 │ ├── Program.cs
10 │ ├── Using.cs
11 │ ├── appsettings.json
12 └── client
13 ├── Dockerfile
14 ├── app
15 │ ├── components
16 │ │ ├── chat.tsx
17 │ │ ├── danger-error.tsx
18 │ │ └── loading.tsx
19 │ ├── hooks
20 │ │ └── useChat.ts
21 │ ├── layout.tsx
22 │ └── page.tsx
23
Este é o principal ponto de entrada da aplicação. Estabelece a aplicação Web, configura os serviços e define o pipeline de middleware.
1using System.Text;
2using System.Text.Json;
3using System.Threading.RateLimiting;
4
5var builder = WebApplication.CreateBuilder(args);
6
7// Add services to the container.
8// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
9builder.Services.AddEndpointsApiExplorer();
10builder.Services.AddServicesExtension();
11builder.Services.AddSwaggerExtension();
12builder.Services.AddCorsExtension(builder.Configuration);
13builder.Services.AddHealthChecksExtension(builder.Configuration);
14builder.Services.AddRateLimiter(options =>
15{
16 options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
17
18 options.AddPolicy("fixed", httpContext =>
19 RateLimitPartition.GetFixedWindowLimiter(
20 partitionKey: httpContext.Connection.RemoteIpAddress?.ToString(),
21 factory: partition => new FixedWindowRateLimiterOptions
22 {
23 PermitLimit = 10,
24 Window = TimeSpan.FromSeconds(10)
25 }));
26});
27
28var app = builder.Build();
29
30// Configure the HTTP request pipeline.
31app.UseSwagger();
32app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "API for a Chat Bot"));
33
34app.MapHealthChecks("/health");
35
36app.UseHttpsRedirection();
37
38app.UseRateLimiter();
39
40app.MapPost("/prompt/{text}", async (
41 string text,
42 IHttpClientFactory factory,
43 HttpContext httpContext) =>
44{
45 var languageModelApiKey = app.Configuration["LANGUAGE_MODEL:API_KEY"];
46 var languageModelUrl = $"{app.Configuration["LANGUAGE_MODEL:URL"]}?key={languageModelApiKey}";
47
48 var payload = new
49 {
50 prompt = new { messages = new[] { new { content = text } } },
51 temperature = 0.1,
52 candidate_count = 1
53 };
54
55 try
56 {
57 using var httpClient = factory.CreateClient();
58 var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
59 var response = await httpClient.PostAsync(languageModelUrl, content);
60 var data = await response.Content.ReadAsStringAsync();
61
62 app.Logger.LogInformation($"Response received from the API: {data}");
63
64 await httpContext.Response.WriteAsync(data);
65 }
66 catch (Exception ex)
67 {
68 app.Logger.LogError(ex, "An error occurred while contacting the API.");
69 }
70})
71.WithName("Generate Language Model Response")
72.WithSummary("Return a Language Model Response")
73.WithDescription("Return a Language Model Response from PaLM API")
74.WithOpenApi(generatedOperation =>
75{
76 var parameter = generatedOperation.Parameters[0];
77 parameter.Description = "The text to be processed by the language model";
78 return generatedOperation;
79})
80.Produces(StatusCodes.Status200OK)
81.Produces(StatusCodes.Status400BadRequest)
82.Produces(StatusCodes.Status500InternalServerError)
83.RequireRateLimiting("fixed");
84
85app.UseCors("AllowClient");
86
87app.Run();
builder.Services.AddServicesExtension():
adiciona serviços personalizados definidos em BuilderExtensions.cs
.builder.Services.AddSwaggerExtension()
: Adiciona Swagger para documentação da API.builder.Services.AddCorsExtension(builder.Configuration)
: adiciona política CORS.builder.Services.AddHealthChecksExtension(builder.Configuration)
: adiciona os health checks.builder.Services.AddRateLimiter(...)
: configura o rate limiting para evitar abusos.app.MapHealthChecks(“/health”)
: mapeia o endpoint de health check.app.UseRateLimiter()
: habilita o middleware de rate limiting.app.MapPost(“/prompt/{text}”, ...)
: define um endpoint para gerar uma resposta da API do Google Gemini.Esse arquivo contém os métodos para adicionar os serviços para o contêiner de injeção de dependência.
1using Microsoft.OpenApi.Models;
2
3namespace API.Extensions;
4
5public static class BuilderExtensions
6{
7 public static void AddSwaggerExtension(this IServiceCollection services) {
8 services.AddSwaggerGen(options => options.SwaggerDoc("v1", new OpenApiInfo
9 {
10 Version = "v1",
11 Title = "Chat bot API",
12 Description = "A ASP.NET Core 8 minimal API to generate messages for a chat bot using PaLM 2 API",
13 }));
14 }
15
16 public static void AddCorsExtension(this IServiceCollection services, IConfiguration config) {
17 var clientUrl = config["Client_Url"] ?? "http://localhost:3000";
18
19 services.AddCors(options => options.AddPolicy(name: "AllowClient", policy =>
20 policy.WithOrigins(clientUrl)
21 .AllowAnyHeader()
22 .WithMethods("POST")));
23 }
24
25 public static void AddHealthChecksExtension(this IServiceCollection services, IConfiguration config) {
26 services.AddHttpClient();
27 services.AddHealthChecks()
28 .AddCheck("google-api", new GoogleColabHealthCheck(services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>(), config, services.BuildServiceProvider().GetRequiredService<ILogger<GoogleColabHealthCheck>>()));
29 }
30
31 public static void AddServicesExtension(this IServiceCollection services) {
32 services.AddHttpClient();
33 }
34}
Este arquivo define um controller de health check personalizado para a API do Google.
1using System.Text;
2
3using Microsoft.Extensions.Diagnostics.HealthChecks;
4
5namespace API.Extensions;
6
7public class GoogleColabHealthCheck(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger<GoogleColabHealthCheck> logger) : IHealthCheck {
8 private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
9 private readonly IConfiguration _configuration = configuration;
10 private readonly ILogger<GoogleColabHealthCheck> _logger = logger;
11
12 public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken) {
13 try
14 {
15 var httpClient = _httpClientFactory.CreateClient();
16 var languageModelApiKey = _configuration["LANGUAGE_MODEL:API_KEY"];
17 var request = new HttpRequestMessage(HttpMethod.Post, $"{_configuration["LANGUAGE_MODEL:URL"]}?key={languageModelApiKey}")
18 {
19 Content = new StringContent(
20 "{ \"contents\":[{\"parts\":[{\"text\":\"hi\"}]}] }",
21 Encoding.UTF8,
22 "application/json")
23 };
24
25 HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
26
27 if (response.IsSuccessStatusCode)
28 {
29 _logger.LogInformation("Google API responded with success status code.");
30 return HealthCheckResult.Healthy();
31 }
32 else
33 {
34 _logger.LogError($"Google API responded with: {response.StatusCode} - {response.ReasonPhrase}");
35 return HealthCheckResult.Unhealthy($"Google API responded with: {response.StatusCode} - {response.ReasonPhrase}");
36 }
37 }
38 catch (Exception ex)
39 {
40 _logger.LogError(ex, "An error occurred while contacting Google API.");
41 return HealthCheckResult.Unhealthy("An error occurred while contacting Google API: " + ex.Message);
42 }
43 }
44}
HealthCheckResult.Healthy()
. Caso contrário, devolve HealthCheckResult.Unhealthy()
com os detalhes do erro.1{
2 "Logging": {
3 "LogLevel": {
4 "Default": "Information",
5 "Microsoft.AspNetCore": "Warning"
6 }
7 },
8 "LANGUAGE_MODEL": {
9 "URL": "https://generativelanguage.googleapis.com/v1beta1/models/chat-bison-001:generateMessage",
10 "API_KEY": ""
11 }
12 "AllowedHosts": "*",
13 "ClientUrl": "http://localhost:3000"
14}
15
Pode-se obter a chave da API e o URL da API REST aqui:
Tutorial: Get started with the Gemini API
Esse arquivo contém diretivas de uso globais para simplificar o código.
1global using API.Extensions;
Agora vamos analisar o código do cliente do chatbot.
1. Criando o projeto:
1cd src
2npx create-next-app@latest client --typescript
3cd client
2. Instalando o TailwindCSS:
1npm install -D tailwindcss postcss autoprefixer
2npx tailwindcss init -p
3. Configurando o TailwindCSS:
tailwind.config.js:
1import type { Config } from 'tailwindcss'
2
3 const config: Config = {
4 content: [
5 './pages/**/*.{js,ts,jsx,tsx,mdx}',
6 './components/**/*.{js,ts,jsx,tsx,mdx}',
7 './app/**/*.{js,ts,jsx,tsx,mdx}',
8 ],
9 theme: {
10 extend: {
11 backgroundImage: {
12 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
13 'gradient-conic':
14 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
15 },
16 animation: {
17 'pulse-slow': 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
18 'pulse-normal': 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite',
19 'pulse-fast': 'pulse 1s cubic-bezier(0.4, 0, 0.6, 1) infinite',
20 },
21 },
22 },
23 plugins: [],
24 }
25 export default config
styles/globals.css:
1@tailwind base;
2@tailwind components;
3@tailwind utilities;
4. Configuração de variáveis de ambiente:
.env.local:
1NEXT_PUBLIC_API_URL=http://localhost:8080
Este é o componente principal do chat que lida com a entrada do usuário, exibe mensagens e mostra os estados de carregamento e erro.
O styling desse component é feito a partir desse componente da Flowbite:
Tailwind CSS Textarea - Flowbite
1'use client'
2
3import React, { useEffect, useRef } from 'react'
4import Loading from './loading'
5import ReactMarkdown from 'react-markdown'
6import DangerError from './danger-error'
7import { useChat } from '../hooks/useChat'
8
9export default function Chat() {
10 const {
11 text,
12 setText,
13 messages,
14 isLoading,
15 errorMessage,
16 handleSubmit,
17 dismissError,
18 } = useChat()
19
20 const inputRef = useRef<HTMLInputElement>(null);
21 const messagesEndRef = useRef<HTMLDivElement>(null)
22
23 useEffect(() => {
24 messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
25 }, [messages])
26
27 useEffect(() => {
28 if (!isLoading) {
29 inputRef.current?.focus();
30 }
31 }, [isLoading]);
32
33 return (
34 <div className='p-6'>
35 {errorMessage && (
36 <DangerError message={errorMessage} dismissError={dismissError} />
37 )}
38 <div className='mb-4 rounded-lg border border-gray-200 p-4 dark:border-gray-700 dark:bg-gray-900'>
39 {messages.map((message, index) => (
40 <div
41 key={index}
42 className={`mb-4 ${message.author === 'Bot' ? 'text-blue-500' : 'text-green-500'}`}
43 >
44 <strong>{message.author}</strong>:{' '}
45 <ReactMarkdown>{message.content}</ReactMarkdown>
46 </div>
47 ))}
48 <div ref={messagesEndRef} />
49 {isLoading && <Loading />}
50 </div>
51 <form onSubmit={handleSubmit}>
52 <label htmlFor='chat' className='sr-only'>
53 Your message
54 </label>
55 <div className='flex items-center rounded-lg bg-gray-50 px-3 py-2 dark:bg-gray-700'>
56 <input
57 id='chat'
58 disabled={isLoading}
59 ref={inputRef}
60 className='mx-4 block w-full rounded-lg border border-gray-300 bg-white p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500'
61 placeholder='Your message...'
62 value={text}
63 onChange={(e) => setText(e.target.value)}
64 />
65 <button
66 type='submit'
67 disabled={isLoading}
68 className='inline-flex cursor-pointer justify-center rounded-full p-2 text-blue-600 hover:bg-blue-100 dark:text-blue-500 dark:hover:bg-gray-600'
69 >
70 <svg
71 className='h-5 w-5 rotate-90 rtl:-rotate-90'
72 aria-hidden='true'
73 xmlns='<http://www.w3.org/2000/svg>'
74 fill='currentColor'
75 viewBox='0 0 18 20'
76 >
77 <path d='m17.914 18.594-8-18a1 1 0 0 0-1.828 0l-8 18a1 1 0 0 0 1.157 1.376L8 18.281V9a1 1 0 0 1 2 0v9.281l6.758 1.689a1 1 0 0 0 1.156-1.376Z' />
78 </svg>
79 <span className='sr-only'>Send message</span>
80 </button>
81 </div>
82 </form>
83 </div>
84 )
85}
Esse componente exibe uma animação de carregamento enquanto aguarda a resposta do bot.
Esse componente é uma modificação refatorada da Flowbite:
Tailwind CSS Skeleton - Flowbite
1interface LoadingBarProps {
2 width: string;
3 animation: string;
4 bgColor: string;
5 marginLeft: string;
6}
7
8const LoadingBar: React.FC<LoadingBarProps> = ({ width, animation, bgColor, marginLeft }) => (
9 <div className={`h-2.5 ${width} ${animation} rounded-full ${bgColor} ${marginLeft}`}></div>
10);
11
12export default function Loading() {
13 const loadingBars = [
14 [
15 { width: 'w-32', animation: 'animate-pulse-slow', bgColor: 'bg-gray-200 dark:bg-gray-700', marginLeft: '' },
16 { width: 'w-24', animation: 'animate-pulse-normal', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
17 { width: 'w-full', animation: 'animate-pulse-fast', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
18 ],
19 [
20 { width: 'w-full', animation: 'animate-pulse-normal', bgColor: 'bg-gray-200 dark:bg-gray-700', marginLeft: '' },
21 { width: 'w-full', animation: 'animate-pulse-slow', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
22 { width: 'w-24', animation: 'animate-pulse-fast', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
23 ],
24 [
25 { width: 'w-full', animation: 'animate-pulse-fast', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: '' },
26 { width: 'w-80', animation: 'animate-pulse-normal', bgColor: 'bg-gray-200 dark:bg-gray-700', marginLeft: 'ms-2' },
27 { width: 'w-full', animation: 'animate-pulse-slow', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
28 ],
29 [
30 { width: 'w-full', animation: 'animate-pulse-slow', bgColor: 'bg-gray-200 dark:bg-gray-700', marginLeft: 'ms-2' },
31 { width: 'w-full', animation: 'animate-pulse-normal', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
32 { width: 'w-24', animation: 'animate-pulse-fast', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
33 ],
34 [
35 { width: 'w-32', animation: 'animate-pulse-normal', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
36 { width: 'w-24', animation: 'animate-pulse-slow', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
37 { width: 'w-full', animation: 'animate-pulse-fast', bgColor: 'bg-gray-200 dark:bg-gray-700', marginLeft: 'ms-2' },
38 ],
39 [
40 { width: 'w-full', animation: 'animate-pulse-fast', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
41 { width: 'w-80', animation: 'animate-pulse-normal', bgColor: 'bg-gray-200 dark:bg-gray-700', marginLeft: 'ms-2' },
42 { width: 'w-full', animation: 'animate-pulse-slow', bgColor: 'bg-gray-300 dark:bg-gray-600', marginLeft: 'ms-2' },
43 ],
44 ];
45
46 return (
47 <div role='status' className='mb-4 space-y-2.5 p-6 dark:bg-gray-900'>
48 {loadingBars.map((bars, index) => (
49 <div key={index} className='flex w-full items-center'>
50 {bars.map((bar, idx) => (
51 <LoadingBar key={idx} {...bar} />
52 ))}
53 </div>
54 ))}
55 <span className='sr-only'>Loading...</span>
56 </div>
57 );
58}
Esse componente exibe uma mensagem de erro quando algo dá errado.
O styling desse component é feito a partir desse componente da Flowbite: Tailwind CSS Alerts - Flowbite
1interface DangerErrorTypes {
2 message: string
3 dismissError: () => void
4}
5
6export default function DangerError({
7 message,
8 dismissError,
9}: DangerErrorTypes) {
10 return (
11 <div id='alert-border-2' className='mb-4 flex items-center border-t-4 border-red-300 bg-red-50 p-4 text-red-800 dark:border-red-800 dark:bg-gray-800 dark:text-red-400' role='alert' >
12 <svg className='h-4 w-4 flex-shrink-0' aria-hidden='true' xmlns='<http://www.w3.org/2000/svg>' fill='currentColor' viewBox='0 0 20 20' >
13 <path d='M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z' />
14 </svg>
15 <div className='ms-3 text-sm font-medium'>{message}</div>
16 <button type='button' onClick={dismissError} className='-mx-1.5 -my-1.5 ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-red-50 p-1.5 text-red-500 hover:bg-red-200 focus:ring-2 focus:ring-red-400 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700' data-dismiss-target='#alert-border-2' aria-label='Close' >
17 <span className='sr-only'>Dismiss</span>
18 <svg className='h-3 w-3' aria-hidden='true' xmlns='<http://www.w3.org/2000/svg>' fill='none' viewBox='0 0 14 14' >
19 <path stroke='currentColor' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' d='m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6' />
20 </svg>
21 </button>
22 </div>
23 )
24}
Esse hook gerencia o estado do chat, incluindo as respostas da API.
1import { useState, useEffect } from 'react'
2import axios, { AxiosError } from 'axios'
3
4const API_URL = process.env.NEXT_PUBLIC_API_URL
5
6type Message = {
7 author: string
8 content: string
9}
10
11export const useChat = () => {
12 const [text, setText] = useState('')
13 const [messages, setMessages] = useState<Message[]>([])
14 const [isLoading, setIsLoading] = useState(false)
15 const [errorMessage, setErrorMessage] = useState<string | null>(null)
16
17 useEffect(() => {
18 const savedMessages = sessionStorage.getItem('messages')
19 if (savedMessages) {
20 setMessages(JSON.parse(savedMessages))
21 }
22 }, [])
23
24 const handleSubmit = (e: React.FormEvent) => {
25 e.preventDefault()
26 setMessages((prevMessages) => {
27 const newMessages = [...prevMessages, { author: 'User', content: text }]
28 sessionStorage.setItem('messages', JSON.stringify(newMessages))
29 return newMessages
30 })
31 setText('')
32 setTimeout(getResponse, 0)
33 }
34
35 const getResponse = async () => {
36 setIsLoading(true)
37 try {
38 const response = await axios.get(`${API_URL}/prompt/${text}`)
39 const data = response.data
40 setMessages((prevMessages) => {
41 const newMessages = [
42 ...prevMessages,
43 { author: 'Bot', content: data.candidates[0].content },
44 ]
45 sessionStorage.setItem('messages', JSON.stringify(newMessages))
46 return newMessages
47 })
48 } catch (error) {
49 const axiosError = error as AxiosError
50 let errorMessage = 'An unexpected error occurred'
51 if (axiosError.response) {
52 errorMessage = axiosError.message
53 }
54 setErrorMessage(errorMessage)
55 }
56 setIsLoading(false)
57 }
58
59 const dismissError = () => {
60 setErrorMessage(null)
61 }
62
63 return {
64 text,
65 setText,
66 messages,
67 isLoading,
68 errorMessage,
69 handleSubmit,
70 dismissError,
71 }
72}
sessionStorage
na renderização inicial.Renderizamos o componente Chat
.
1import Chat from '../components/chat'
2
3export default function Home() {
4 return <Chat />
5}
Por fim, executamos a aplicação:
1npm run dev
Vá até http://localhost:3000
no navegador para ver o client do chatbot em funcionamento.
Esse código implementa um cliente Next.js com TypeScript e TailwindCSS para um chatbot. O componente Chat
manipula a entrada do usuário, exibe mensagens e gerência os estados de carregamento e erro. O hook useChat
gerencia o estado do chat e interage com a API do chatbot.
Inspirado pelo vídeo - PaLM 2 API Course – Build Generative AI Apps da freeCodeCamp