diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..98232e4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.cs] + +# SKEXP0070: Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +dotnet_diagnostic.SKEXP0070.severity = silent diff --git a/AuthMiddleware.cs b/AuthMiddleware.cs new file mode 100644 index 0000000..350eb19 --- /dev/null +++ b/AuthMiddleware.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; + +namespace ChatApi +{ + public class AuthMiddleware + { + public void ConfigureServices(IServiceCollection services) + { + } + } +} diff --git a/ChatApi.csproj b/ChatApi.csproj new file mode 100644 index 0000000..2d9ea86 --- /dev/null +++ b/ChatApi.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + enable + enable + 10e5023f-8f45-46d6-8637-bc2127842068 + Linux + . + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChatApi.csproj.user b/ChatApi.csproj.user new file mode 100644 index 0000000..983ecfc --- /dev/null +++ b/ChatApi.csproj.user @@ -0,0 +1,9 @@ + + + + http + + + ProjectDebugger + + \ No newline at end of file diff --git a/ChatApi.http b/ChatApi.http new file mode 100644 index 0000000..b2d56ee --- /dev/null +++ b/ChatApi.http @@ -0,0 +1,6 @@ +@ChatApi_HostAddress = http://localhost:5020 + +GET {{ChatApi_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/ChatApi.sln b/ChatApi.sln new file mode 100644 index 0000000..7f588b2 --- /dev/null +++ b/ChatApi.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35122.118 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatApi", "ChatApi.csproj", "{B5287933-4BFA-4EC1-8522-393864C46B1F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B5287933-4BFA-4EC1-8522-393864C46B1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5287933-4BFA-4EC1-8522-393864C46B1F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5287933-4BFA-4EC1-8522-393864C46B1F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5287933-4BFA-4EC1-8522-393864C46B1F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {332EC001-9CEA-4261-9DE0-110EEB502728} + EndGlobalSection +EndGlobal diff --git a/ChatHistoryService.cs b/ChatHistoryService.cs new file mode 100644 index 0000000..5eee0ea --- /dev/null +++ b/ChatHistoryService.cs @@ -0,0 +1,92 @@ +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using System.Text.Json; + +namespace ChatApi +{ + public class ChatHistoryService + { + private readonly Dictionary _keyValues = new Dictionary(); + public ChatHistoryService() + { + } + + public ChatHistory Get(string sessionId) + { + //var msg = new List(); + ////msg.Add(new ChatMessageContent(AuthorRole.System, "Your name is SuperChat. \nYou only generate answers using portuguese.\nYou are friendly and polite.\nYou speak only Brazilian Portuguese.\nYou never create a response in English. Always brazilian portuguese.")); + //msg.Add(new ChatMessageContent(AuthorRole.System, "Seu nome é SuperChat.")); + //msg.Add(new ChatMessageContent(AuthorRole.System, "Você só gera respostas usando português do Brasil.")); + //msg.Add(new ChatMessageContent(AuthorRole.System, "Você fala apenas português brasileiro.")); + //msg.Add(new ChatMessageContent(AuthorRole.System, "Você nunca cria respostas em inglês, mas sempre em português brasileiro.")); + //string json = JsonSerializer.Serialize(msg); + //var history = new ChatHistory(JsonSerializer.Deserialize>(json)); + + //return history; + + if (_keyValues.ContainsKey(sessionId)) + { + var history = _keyValues[sessionId]; + return history; + } + else + { + var msg = new List(); + TestePrompt(msg); + string json = JsonSerializer.Serialize(msg); + var history = new ChatHistory(JsonSerializer.Deserialize>(json)); + _keyValues[sessionId] = history; + return _keyValues[sessionId]; + } + } + + public ChatHistory GetSumarizer(string sessionId) + { + if (_keyValues.ContainsKey(sessionId)) + { + var history = _keyValues[sessionId]; + return history; + } + else + { + var msg = new List(); + PromptIan(msg); + string json = JsonSerializer.Serialize(msg); + var history = new ChatHistory(JsonSerializer.Deserialize>(json)); + _keyValues[sessionId] = history; + return _keyValues[sessionId]; + } + } + + public void UpdateHistory(string sessionId, ChatHistory history) + { + _keyValues[sessionId] = history; + } + + public void TestePromptBot(List msg) + { + msg.Add(new ChatMessageContent(AuthorRole.System, "Seu nome é Rosa, uma vendedora de bolos que está atendendo seus clientes. ")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Você tem apenas os seguintes sabores de bolo: chocolate, baunilha e morango. ")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Excepcionalmente hoje, o sabor morango está fora de estoque. Você não tem mais morangos. ")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Cada fatia de bolo custa 5 reais. \n")); + msg.Add(new ChatMessageContent(AuthorRole.User, "Responda sempre em portugues do Brasil as minhas perguntas.")); + } + + public void PromptIan(List msg) + { + msg.Add(new ChatMessageContent(AuthorRole.System, "Seu nome é Ian, um assistente ajuda empresas de Consultoria e RH. ")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Você responde sempre em português do Brasil.")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Em breve, você será capaz de consultar o linkedin.")); + msg.Add(new ChatMessageContent(AuthorRole.User, "Use sempre portugues do Brasil.")); + } + + public void TestePrompt(List msg) + { + msg.Add(new ChatMessageContent(AuthorRole.System, "Seu nome é Ian.")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Você só gera respostas usando português do Brasil.")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Você fala apenas português brasileiro.")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Você nunca cria respostas em inglês, mas sempre em português brasileiro.")); + msg.Add(new ChatMessageContent(AuthorRole.User, "Responda sempre em portugues do Brasil as minhas perguntas.")); + } + } +} diff --git a/ChatHistoryStore.cs b/ChatHistoryStore.cs new file mode 100644 index 0000000..16f992c --- /dev/null +++ b/ChatHistoryStore.cs @@ -0,0 +1,7 @@ +namespace ChatApi +{ + public class ChatHistoryStore + { + + } +} diff --git a/ChatRequest.cs b/ChatRequest.cs new file mode 100644 index 0000000..b6dd109 --- /dev/null +++ b/ChatRequest.cs @@ -0,0 +1,7 @@ +using System; + +public class ChatRequest +{ + public string SessionId { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; +} diff --git a/Controllers/ChatController.cs b/Controllers/ChatController.cs new file mode 100644 index 0000000..2a16dfc --- /dev/null +++ b/Controllers/ChatController.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Connectors.InMemory; +using System.Globalization; +using System.Text; +using MongoDB.Driver; +using System.IO; +using System.Text.RegularExpressions; +using ChatApi.Services; +using ChatApi.Services.ResponseService; +using ChatApi.Services.Classifier; +using Microsoft.AspNetCore.Authorization; +using System.Security.Claims; +using ChatApi.Models; + +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace ChatApi.Controllers +{ + [Authorize] + [ApiController] + [Route("[controller]")] + public class ChatController : ControllerBase + { + private readonly ILogger _logger; + private readonly TextFilter _textFilter; + private readonly ResponseFactory _responseFactory; + private readonly ClassifierPersistence _classifierPersistence; + + public ChatController( + ILogger logger, + TextFilter textFilter, + ResponseFactory responseFactory, + ClassifierPersistence classifierPersistence) + { + _logger = logger; + _textFilter = textFilter; + _responseFactory = responseFactory; + _classifierPersistence = classifierPersistence; + } + + [HttpGet(Name = "Response")] + public async Task Get([FromQuery] ChatRequest chatRequest) + { + var userData = UserData.Create(User); + var textClassifier = new TextClassifier(_textFilter, _classifierPersistence); + var textType = await textClassifier.ClassifyQuestion(chatRequest.SessionId, chatRequest.Message); + var responseText = _responseFactory.GetService(textType); + var response = await responseText.GetResponse(HttpContext, userData, chatRequest.SessionId, chatRequest.Message); + return response; + } + + [HttpPost(Name = "LoadDBData")] + public async Task SaveData([FromQuery] DBLoadRequest loadRequest) + { + //string readText = System.IO.File.ReadAllText("C:\\vscode\\ChatApi\\bin\\Debug\\net8.0\\Servicos.txt"); + string readText = loadRequest.Content; + await SalvarTextoComEmbeddingNoMongoDB(readText); + } + + + async Task SalvarTextoComEmbeddingNoMongoDB(string textoCompleto) + { + var textoArray = new List(); + string[] textolinhas = textoCompleto.Split( + new string[] { "\n" }, + StringSplitOptions.None + ); + + var title = textolinhas[0]; + + var builder = new StringBuilder(); + foreach (string line in textolinhas) + { + if (line.StartsWith("**") || line.StartsWith("\r**")) + { + if (builder.Length > 0) + { + textoArray.Add(title.Replace("**", "").Replace("\r", "") + ": " + Environment.NewLine + builder.ToString()); + builder = new StringBuilder(); + title = line; + } + } + else + { + builder.AppendLine(line); + } + } + } + } +} +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/Controllers/ChatController.cs~RF39ccb1.TMP b/Controllers/ChatController.cs~RF39ccb1.TMP new file mode 100644 index 0000000..f682375 --- /dev/null +++ b/Controllers/ChatController.cs~RF39ccb1.TMP @@ -0,0 +1,68 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Memory; +using Microsoft.SemanticKernel.Connectors.InMemory; + +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace ChatApi.Controllers +{ + [ApiController] + [Route("[controller]")] + public class ChatController : ControllerBase + { + private readonly ILogger _logger; + private readonly ChatHistoryService _chatHistoryService; + private readonly IChatCompletionService _chatCompletionService; + private readonly Kernel _kernel; + + public ChatController(ILogger logger, ChatHistoryService chatHistoryService, IChatCompletionService chatCompletionService, Kernel kernel) + { + _logger = logger; + _chatHistoryService = chatHistoryService; + _chatCompletionService = chatCompletionService; + _kernel = kernel; + } + + [HttpGet(Name = "Response")] + public async Task Get([FromQuery] ChatRequest chatRequest) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + + stopWatch.Start(); + SessionIdStore sessionIdStore = new SessionIdStore(HttpContext); + var sessionId = sessionIdStore.GetSessionId(); + var history = _chatHistoryService.Get(sessionId); + + history.AddUserMessage(chatRequest.Message); + + var embeddingGenerator = _kernel.GetRequiredService(); + // Setup a memory store and create a memory out of it +#pragma warning disable SKEXP0020 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + var memoryStore = new InMemoryVectorStore(); +#pragma warning restore SKEXP0020 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + var memory = new SemanticTextMemory(memoryStore, embeddingGenerator); + // Loading it for Save, Recall and other methods + _kernel.ImportPluginFromObject(new TextMemoryPlugin(memory)); + string MemoryCollectionName = "MyCustomDataCollection"; + + var option = new PromptExecutionSettings + { + + Memory = memory, + MemoryCollectionName = MemoryCollectionName + }; + + var response = await _chatCompletionService.GetChatMessageContentAsync(history); + history.AddMessage(response.Role, response.Content ?? ""); + + _chatHistoryService.UpdateHistory(sessionId, history); + + stopWatch.Stop(); + return $"{response.Content ?? ""}\n\nTempo: {stopWatch.ElapsedMilliseconds/1000}s"; + } + } +} +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/DBLoadRequest.cs b/DBLoadRequest.cs new file mode 100644 index 0000000..8be824e --- /dev/null +++ b/DBLoadRequest.cs @@ -0,0 +1,7 @@ +namespace ChatApi +{ + public class DBLoadRequest + { + public string Content { get; set; } = string.Empty; + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f4b0d66 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER app +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["ChatApi.csproj", "."] +RUN dotnet restore "./ChatApi.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "./ChatApi.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./ChatApi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "ChatApi.dll"] \ No newline at end of file diff --git a/DomvsDatabaseSettings.cs b/DomvsDatabaseSettings.cs new file mode 100644 index 0000000..eeb97bd --- /dev/null +++ b/DomvsDatabaseSettings.cs @@ -0,0 +1,15 @@ +namespace ChatApi +{ + public class DomvsDatabaseSettings + { + public string ConnectionString { get; set; } = null!; + + public string DatabaseName { get; set; } = null!; + + public string SharepointCollectionName { get; set; } = null!; + + public string ChatBotRHCollectionName { get; set; } = null!; + + public string ClassifierCollectionName { get; set; } = null!; + } +} diff --git a/Infra/Result.cs b/Infra/Result.cs new file mode 100644 index 0000000..0c9055a --- /dev/null +++ b/Infra/Result.cs @@ -0,0 +1,56 @@ +namespace ChatApi.Infra +{ + public class Result + { + protected Result(bool success, string error) + { + if (success && error != string.Empty) + throw new InvalidOperationException(); + if (!success && error == string.Empty) + throw new InvalidOperationException(); + Success = success; + Error = error; + } + + public bool Success { get; } + public string Error { get; } + public bool IsFailure => !Success; + + public static Result Fail(string message) + { + return new Result(false, message); + } + + public static Result Fail(string message) + { + return new Result(default, false, message); + } + + public static Result Ok() + { + return new Result(true, string.Empty); + } + + public static Result Ok(T value) + { + return new Result(value, true, string.Empty); + } + } + + public class Result : Result + { + protected internal Result(T value, bool success, string error) + : base(success, error) + { + Value = value; + } + + protected internal Result(T value) + : base(true, "") + { + Value = value; + } + + public T Value { get; set; } + } +} diff --git a/Models/UserData.cs b/Models/UserData.cs new file mode 100644 index 0000000..a1e9052 --- /dev/null +++ b/Models/UserData.cs @@ -0,0 +1,30 @@ +using System.Security.Claims; + +namespace ChatApi.Models +{ + public class UserData + { + private UserData() + { + } + + public string Email { get; set; } + public string Name { get; set; } + + public static UserData Create(ClaimsPrincipal user) + { + var email = user.FindFirst(ClaimTypes.Email)?.Value + ?? user.FindFirst("email")?.Value; + + var name = user.FindFirst(ClaimTypes.Name)?.Value + ?? user.FindFirst("name")?.Value; + + + return new UserData + { + Email = email, + Name = name + }; + } + } +} diff --git a/Models/UserData.cs~RF3dab6a.TMP b/Models/UserData.cs~RF3dab6a.TMP new file mode 100644 index 0000000..c6f962b --- /dev/null +++ b/Models/UserData.cs~RF3dab6a.TMP @@ -0,0 +1,30 @@ +using System.Security.Claims; + +namespace ChatApi.Models +{ + public class UserData + { + private UserData() + { + } + + public int Email { get; set; } + public int Name { get; set; } + + public static UserData Create(ClaimsPrincipal user) + { + var email = user.FindFirst(ClaimTypes.Email)?.Value + ?? user.FindFirst("email")?.Value; + + var name = user.FindFirst(ClaimTypes.Name)?.Value + ?? user.FindFirst("name")?.Value; + + + return new UserData + { + Email = email, + Name = name + }; + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..52e4030 --- /dev/null +++ b/Program.cs @@ -0,0 +1,157 @@ +using ChatApi; +using ChatApi.Services; +using ChatApi.Services.Bot; +using ChatApi.Services.Bot.Structs; +using ChatApi.Services.Classifier; +using ChatApi.Services.ResponseService; +using ChatApi.Settings; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using OllamaSharp; + +#pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.Configure( +builder.Configuration.GetSection("DomvsDatabase")); + +builder.Services.Configure( +builder.Configuration.GetSection("ChatRHSettings")); + +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + + +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); +//var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin"); +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinyllama", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("starling-lm", new Uri("http://localhost:11435")); + +//ServerSpace - GPT Service +builder.Services.AddOpenAIChatCompletion("openchat-3.5-0106", new Uri("https://gpt.serverspace.com.br/v1/chat/completions"), "tIAXVf3AkCkkpSX+PjFvktfEeSPyA1ZYam50UO3ye/qmxVZX6PIXstmJsLZXkQ39C33onFD/81mdxvhbGHm7tQ=="); + +builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOpenAIChatCompletion("gpt-4o-mini", "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"); +//builder.Services.AddGoogleAIGeminiChatCompletion("gemini-1.5-flash-latest", "AIzaSyDKBMX5yW77vxJFVJVE-5VLxlQRxCepck8"); + +//Anthropic / Claude +//builder.Services.AddAnthropicChatCompletion( +// modelId: "claude-3-5-sonnet-latest", // ou outro modelo Claude desejado +// apiKey: "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" +//); + +builder.Services.AddKernel(); +//builder.Services.AddKernel() +// .AddOllamaChatCompletion("phi3", new Uri("http://localhost:11435")) +// .AddOllamaTextEmbeddingGeneration() +// .Build(); + +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://192.168.0.150:11436")); + +builder.Services.AddHttpClient(); + +var tenantId = builder.Configuration.GetSection("AppTenantId"); +var clientId = builder.Configuration.GetSection("AppClientID"); +builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = $"https://login.microsoftonline.com/{tenantId}/v2.0"; + options.Audience = "api://" + clientId; // Client ID da sua API + + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = $"https://login.microsoftonline.com/{tenantId}/v2.0", + ValidAudience = "api://" + clientId + }; + }); + +builder.Services.AddControllers(); + +//builder.Services.AddAuthentication(options => +// { +// options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; +// options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +// }) +// .AddJwtBearer(options => +// { +// // Configurações anteriores... + +// // Eventos para log e tratamento de erros +// options.Events = new JwtBearerEvents +// { +// OnAuthenticationFailed = context => +// { +// // Log de erros de autenticação +// Console.WriteLine($"Erro de autenticação: {context.Exception.Message}"); +// return Task.CompletedTask; +// }, +// OnTokenValidated = context => +// { +// // Validações adicionais se necessário +// return Task.CompletedTask; +// } +// }; +// }); + + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Use(async (context, next) => +{ + var cookieOpt = new CookieOptions() + { + Path = "/", + Expires = DateTimeOffset.UtcNow.AddDays(1), + IsEssential = true, + HttpOnly = false, + Secure = false, + }; + + await next(); +}); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.Run(); + +#pragma warning restore SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/Program.cs~RF419917.TMP b/Program.cs~RF419917.TMP new file mode 100644 index 0000000..b9099d9 --- /dev/null +++ b/Program.cs~RF419917.TMP @@ -0,0 +1,61 @@ +using ChatApi; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using OllamaSharp; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddSingleton(); +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); +//var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin"); +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinyllama", new Uri("http://localhost:11435")); +builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11435")); +builder.Services.AddKernel(); +//builder.Services.AddKernel() +// .AddOllamaChatCompletion("phi3", new Uri("http://localhost:11435")) +// .AddOllamaTextEmbeddingGeneration() +// .Build(); + +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://192.168.0.150:11436")); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Use(async (context, next) => +{ + var cookieOpt = new CookieOptions() + { + Path = "/", + Expires = DateTimeOffset.UtcNow.AddDays(1), + IsEssential = true, + HttpOnly = false, + Secure = false, + }; + + await next(); +}); + +app.Run(); diff --git a/Program.cs~RF68750a0.TMP b/Program.cs~RF68750a0.TMP new file mode 100644 index 0000000..e76aa24 --- /dev/null +++ b/Program.cs~RF68750a0.TMP @@ -0,0 +1,98 @@ +using ChatApi; +using ChatApi.Services; +using ChatApi.Services.Bot; +using ChatApi.Services.Bot.Structs; +using ChatApi.Services.Classifier; +using ChatApi.Services.ResponseService; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using OllamaSharp; + +#pragma warning disable SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.Configure( +builder.Configuration.GetSection("DomvsDatabase")); + +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + + +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); +//var apiClient = new OllamaApiClient(new Uri("http://localhost:11435"), "tinydolphin"); +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinydolphin", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("tinyllama", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("starling-lm", new Uri("http://localhost:11435")); + +//ServerSpace - GPT Service +builder.Services.AddOpenAIChatCompletion("openchat-3.5-0106", new Uri("https://gpt.serverspace.com.br/v1/chat/completions"), "tIAXVf3AkCkkpSX+PjFvktfEeSPyA1ZYam50UO3ye/qmxVZX6PIXstmJsLZXkQ39C33onFD/81mdxvhbGHm7tQ=="); + +builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://localhost:11435")); +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://localhost:11435")); +//builder.Services.AddOpenAIChatCompletion("gpt-4o-mini", "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"); +//builder.Services.AddGoogleAIGeminiChatCompletion("gemini-1.5-flash-latest", "AIzaSyDKBMX5yW77vxJFVJVE-5VLxlQRxCepck8"); + +//Anthropic / Claude +//builder.Services.AddAnthropicChatCompletion( +// modelId: "claude-3-5-sonnet-latest", // ou outro modelo Claude desejado +// apiKey: "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" +//); + +builder.Services.AddKernel(); +//builder.Services.AddKernel() +// .AddOllamaChatCompletion("phi3", new Uri("http://localhost:11435")) +// .AddOllamaTextEmbeddingGeneration() +// .Build(); + +//builder.Services.AddOllamaChatCompletion("phi3.5", new Uri("http://192.168.0.150:11436")); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Use(async (context, next) => +{ + var cookieOpt = new CookieOptions() + { + Path = "/", + Expires = DateTimeOffset.UtcNow.AddDays(1), + IsEssential = true, + HttpOnly = false, + Secure = false, + }; + + await next(); +}); + +app.Run(); + +#pragma warning restore SKEXP0010 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..ed836dc --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5020" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7163;http://localhost:5020" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:61198", + "sslPort": 44305 + } + } +} \ No newline at end of file diff --git a/Services/Bot/5vb0vxog.ejb~ b/Services/Bot/5vb0vxog.ejb~ new file mode 100644 index 0000000..bcd8fd4 --- /dev/null +++ b/Services/Bot/5vb0vxog.ejb~ @@ -0,0 +1,64 @@ +using ChatApi.Services.Bot.Structs; + +namespace ChatApi.Services.Bot +{ + public class ChatBotRHCall + { + private readonly ChatBot _chatbot; + + Dictionary _opcoesSolicitação = + new Dictionary { + { "Inclusão/Exclusão Alura", "Inclusão/Exclusão Alura"}, + { "Inclusão/Exclusão Auxílio Idioma", "Inclusão/Exclusão Auxílio Idioma"}, + { "Inclusão/Exclusão Plano Odonto", "Inclusão/Exclusão Plano Odonto"}, + //{ "Envio de Atestado", "Envio de Atestado"}, + //{ "Envio de Reembolso Idioma", "Envio de Reembolso Idioma"}, + //{ "Afastamento e Licenças", "Afastamento e Licenças"}, + { "Dúvidas Plano de Saúde", "Dúvidas Plano de Saúde"}, + { "Dúvidas Folha de Pagamento", "Dúvidas Folha de Pagamento"}, + { "Ajuste Cargo/Salário", "Ajuste Cargo/Salário"}, + { "Feedbacks, reclamações", "1 - Feedbacks, reclamações"}, + { "Feedz - Dúvida 1:1", "Feedz - Dúvida 1:1"}, + { "Feedz - Dúvida Feedback", "Feedz - Dúvida Feedback"}, + { "Feedz - Cadastro de Objetivo", "Feedz - Cadastro de Objetivo"}, + { "Feedz - Erro Cadastro", "Feedz - Erro Cadastro"}, + { "Feedz - Outros", "Feedz - Outros"}, + { "Ciclo de Performance", "Ciclo de Performance"}, + { "Declaração Bolsa Faculdade", "Declaração Bolsa Faculdade"}, + { "Inclusão/Exclusão Programa de Especialização", "Inclusão/Exclusão Programa de Especialização"}, + //{ "Envio de Reembolso Especialização", "Envio de Reembolso Especialização"}, + { "DayOff", "DayOff"} + }; + + public ChatBotRHCall(ChatBot chatbot) + { + _chatbot = chatbot; + AddNumbersBeforShow(_opcoesSolicitação); + + _chatbot.AddQuestion(new Question( + "Qual seu número de celular (com WhatsApp)?", + resposta => !string.IsNullOrEmpty(resposta) && resposta.Length >= 10)); + + _chatbot.AddQuestion(new Question( + "Qual o tipo de solicitação (1 para Reembolso de Idioma, 2 para Reembolso Alura, 3 para Dúvidas)?", + resposta => resposta == "1" || resposta == "2" || resposta == "3")); + + _chatbot.AddQuestion(new Question( + "Texto/Descrição da solicitação (em caso de dúvidas)", + resposta => !string.IsNullOrEmpty(resposta))); + + _chatbot.AddQuestion(new Question( + "Tudo bem? Posso enviar sua solicitação? Ou prefere que eu tente fazer algum ajuste no texto da descrição?", + resposta => !string.IsNullOrEmpty(resposta))); + } + + private void AddNumbersBeforShow(Dictionary dict) + { + int optionNumber = 1; + foreach (var key in dict.Keys) + { + dict[key] = $"{optionNumber} - {dict[key]}"; + } + } + } +} diff --git a/Services/Bot/ActionCreateCall.cs b/Services/Bot/ActionCreateCall.cs new file mode 100644 index 0000000..001287f --- /dev/null +++ b/Services/Bot/ActionCreateCall.cs @@ -0,0 +1,63 @@ +using ChatApi.Infra; +using ChatApi.Services.Bot.Structs; +using ChatApi.Settings; +using System.Text; + +namespace ChatApi.Services.Bot +{ + public class ActionCreateCall : IActionCall + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly ChatRHSettings _configuration; + private dynamic _callRH; + + public ActionCreateCall(IHttpClientFactory httpClientFactory, ChatRHSettings configuration) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + } + + public Task> Call() + { + var httpClient =_httpClientFactory.CreateClient(); + var url = new Uri(new Uri(_configuration.Url), _configuration.Create); + var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Content = new StringContent(System.Text.Json.JsonSerializer.Serialize(_callRH), Encoding.UTF8, "application/json"); + var response = httpClient.SendAsync(request).Result; + + if (response.IsSuccessStatusCode) + { + var content = response.Content.ReadAsStringAsync().Result; + return Task.FromResult(Result.Ok(content)); + } + + var errorContent = ""; + try + { + errorContent = response.Content.ReadAsStringAsync().Result; + } + catch (Exception ex) + { + errorContent = "Náo foi possivel criar sua solicitação. Reinicie."; + } + + return Task.FromResult(Result.Fail(errorContent)); + } + + public Task Populate(Dictionary? knowParameters, Dictionary answers) + { + if (knowParameters == null) return Task.FromResult(Result.Fail("Nome e/ou email não foram informados.")); + + _callRH = new + { + Nome = knowParameters["Nome"], + Email = knowParameters["Email"], + WhatsApp = answers[0], + TipoSolicitacao = answers[1], + Descricao = answers[2], + }; + + return Task.FromResult(Result.Ok()); + } + } +} diff --git a/Services/Bot/ChatBotRHCall.cs b/Services/Bot/ChatBotRHCall.cs new file mode 100644 index 0000000..b4adb1a --- /dev/null +++ b/Services/Bot/ChatBotRHCall.cs @@ -0,0 +1,120 @@ +using ChatApi.Models; +using ChatApi.Services.Bot.Structs; +using ChatApi.Settings; +using Microsoft.Extensions.Options; +using Microsoft.SemanticKernel.ChatCompletion; +using System.Text; + +namespace ChatApi.Services.Bot +{ + public class ChatBotRHCall + { + private ChatBot _chatbot; + private readonly IChatCompletionService _chatCompletionService; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IOptions _chatRHApiConfig; + public UserData UserData { get; set; } + + Dictionary _opcoesSolicitação = + new Dictionary { + { "Inclusão/Exclusão Alura", "Inclusão/Exclusão Alura"}, + { "Inclusão/Exclusão Auxílio Idioma", "Inclusão/Exclusão Auxílio Idioma"}, + { "Inclusão/Exclusão Plano Odonto", "Inclusão/Exclusão Plano Odonto"}, + //{ "Envio de Atestado", "Envio de Atestado"}, + //{ "Envio de Reembolso Idioma", "Envio de Reembolso Idioma"}, + //{ "Afastamento e Licenças", "Afastamento e Licenças"}, + { "Dúvidas Plano de Saúde", "Dúvidas Plano de Saúde"}, + { "Dúvidas Folha de Pagamento", "Dúvidas Folha de Pagamento"}, + { "Ajuste Cargo/Salário", "Ajuste Cargo/Salário"}, + { "Feedbacks, reclamações", "Feedbacks, reclamações"}, + { "Feedz - Dúvida 1:1", "Feedz - Dúvida 1:1"}, + { "Feedz - Dúvida Feedback", "Feedz - Dúvida Feedback"}, + { "Feedz - Cadastro de Objetivo", "Feedz - Cadastro de Objetivo"}, + { "Feedz - Erro Cadastro", "Feedz - Erro Cadastro"}, + { "Feedz - Outros", "Feedz - Outros"}, + { "Ciclo de Performance", "Ciclo de Performance"}, + { "Declaração Bolsa Faculdade", "Declaração Bolsa Faculdade"}, + { "Inclusão/Exclusão Programa de Especialização", "Inclusão/Exclusão Programa de Especialização"}, + //{ "Envio de Reembolso Especialização", "Envio de Reembolso Especialização"}, + { "DayOff", "DayOff"} + }; + private readonly IOptions _config; + + public ChatBotRHCall( + IOptions config, + IChatCompletionService chatCompletionService, + IHttpClientFactory httpClientFactory, + IOptions chatRHApiConfig) + { + _chatCompletionService = chatCompletionService; + _httpClientFactory = httpClientFactory; + _chatRHApiConfig = chatRHApiConfig; + _config = config; + } + public string SetAnswer(string sessionId, string resposta) + { + if (_chatbot==null) + { + this.Init(sessionId); + } + + return _chatbot.SetAnswer(resposta); + } + + public async Task GetNextQuestion() + { + return await _chatbot.GetNextQuestion(); + } + + public string GetCurrentQuestion() + { + return _chatbot.GetCurrentQuestion(); + } + + public bool HasNextQuestion() + { + return _chatbot.HasNextQuestion(); + } + + public void Init(string sessionId) + { + var persist = ChatPersistence.Create(_config.Value).SetBotName("RHCall"); + var actionOnEnd = new ActionCreateCall(_httpClientFactory, _chatRHApiConfig.Value); + _chatbot = new ChatBot(persist, sessionId, this.UserData, _chatCompletionService, actionOnEnd); + + AddNumbersBeforShow(_opcoesSolicitação); + + _chatbot.AddQuestion(new Question( + "Você quer fazer uma solicitação ao RH, certo??", + resposta => !string.IsNullOrEmpty(resposta))); + + _chatbot.AddQuestion(new Question( + "Qual seu número de celular (com WhatsApp)?", + resposta => !string.IsNullOrEmpty(resposta) && resposta.Length >= 10)); + + var builder = new StringBuilder(); + _opcoesSolicitação.Select(s => s.Value).ToList().ForEach(s => builder.AppendLine(s)); + + _chatbot.AddQuestion(new Question( + $"Indique o tipo de solicitação: \n {builder}", + resposta => resposta == "1" || resposta == "2" || resposta == "3")); + + _chatbot.AddQuestion(new Question( + "Texto/Descrição da solicitação (em caso de dúvidas)", + resposta => !string.IsNullOrEmpty(resposta))); + + _chatbot.AddQuestion(new Question( + "Tudo bem? Posso enviar sua solicitação? Ou prefere que eu tente fazer algum ajuste no texto da descrição?", + resposta => !string.IsNullOrEmpty(resposta))); + } + private void AddNumbersBeforShow(Dictionary dict) + { + int optionNumber = 1; + foreach (var key in dict.Keys) + { + dict[key] = $"{optionNumber} - {dict[key]}"; + optionNumber++; + } + } + } +} diff --git a/Services/Bot/Structs/ChatBot.cs b/Services/Bot/Structs/ChatBot.cs new file mode 100644 index 0000000..7885d1f --- /dev/null +++ b/Services/Bot/Structs/ChatBot.cs @@ -0,0 +1,148 @@ +using Amazon.Auth.AccessControlPolicy; +using ChatApi.Models; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace ChatApi.Services.Bot.Structs +{ + public class ChatBot + { + private readonly List _questions; + private readonly ChatPersistence _persistence; + private readonly Dictionary _answers; + private readonly IChatCompletionService _chatCompletionService; + private readonly IActionCall _actionCall; + private readonly string _usuarioId; + private readonly UserData _userData; + private Dictionary _knowParameters; + private int _indiceAtual; + + public ChatBot(ChatPersistence persistence, string usuarioId, UserData userData, IChatCompletionService chatCompletionService, IActionCall actionCall) + { + _chatCompletionService = chatCompletionService; + _actionCall = actionCall; + _questions = new List(); + _answers = new Dictionary(); + _knowParameters = new Dictionary(); + _indiceAtual = 0; + _persistence = persistence; + _usuarioId = usuarioId; + _userData = userData; + var estado = _persistence.GetState(_usuarioId); + if (estado != null) + { + _indiceAtual = estado.IndicePerguntaAtual; + _answers = estado.Respostas; + } + else + { + _indiceAtual = 0; + } + } + + public void SetParameter(string key, string value) + { + _knowParameters[key] = value; + } + + public void AddQuestion(Question Question) + { + _questions.Add(Question); + } + + public string SetAnswer(string resposta) + { + if (!this.HasNextQuestion()) return ""; + + var state = _persistence.GetState(_usuarioId); + if (state != null) + { + _questions[_indiceAtual].CountTry = state.TentativasPerguntaAtual; + } + else + { + _knowParameters = new Dictionary() + { + { "Nome", _userData.Name }, + { "Email", _userData.Email } + }; + } + + var perguntaAtual = _questions[_indiceAtual]; + var testByChat = TestAnswerByChat(resposta); + var abort = TestIfWantToAbort(resposta); + var validResp = perguntaAtual.TryToAnswer(resposta); + + if (string.IsNullOrEmpty(validResp) && !abort.Contains("ABORTAR") && perguntaAtual.Validator(resposta) && testByChat.Contains("SIM")) + { + _answers[_indiceAtual] = resposta; + _indiceAtual++; + _persistence.SaveState(new ChatState + { + Id = _usuarioId, + UsuarioId = _usuarioId, + IndicePerguntaAtual = _indiceAtual, + TentativasPerguntaAtual = _questions[_indiceAtual].CountTry, + Respostas = new Dictionary(_answers) + }); + + return ""; + } + else if (validResp.Contains("reiniciar") || abort.Contains("ABORTAR")) + { + _persistence.DeleteState(_usuarioId); + return "REINICIAR"; + } + + _persistence.SaveState(new ChatState + { + Id = _usuarioId, + UsuarioId = _usuarioId, + IndicePerguntaAtual = _indiceAtual, + TentativasPerguntaAtual = _questions[_indiceAtual].CountTry, + DadosConhecidos = _knowParameters, + Respostas = new Dictionary(_answers) + }); + + return $"Resposta inválida. Tente novamente. {(testByChat.Contains("NÃO") ? "Motivo: " + testByChat.Replace("NÂO", "") : "")} \n\n {perguntaAtual.Text}"; + } + + public string GetCurrentQuestion() => _questions[_indiceAtual].Text; + public async Task GetNextQuestion() + { + while (_indiceAtual < _questions.Count) + { + var perguntaAtual = _questions[_indiceAtual]; + if (perguntaAtual.MustShow(_answers)) + { + return perguntaAtual.Text; + } + _indiceAtual++; + } + + var resp = await _actionCall.Populate(_knowParameters, _answers); + if (!resp.Success) return "Obrigado por responder a nossas perguntas!"; + + var result = await _actionCall.Call(); + + return result.Success ? result.Value : "Obrigado por responder a nossas perguntas!"; + } + + public string TestAnswerByChat(string resposta) + { + var resp = _chatCompletionService.GetChatMessageContentAsync($"Por favor, responda com SIM/NÂO e o motivo para eu saber se a pergunta: ({this.GetCurrentQuestion()}) foi respondida pelo usuário quando ele digitou: ({resposta})? É um chatbot, por isso preciso saber apenas se a resposta faz sentido e/ou tem o formato certo.").Result; + return resp.Content; + } + + public string TestIfWantToAbort(string resposta) + { + if (_indiceAtual==0) return ""; + var resp = _chatCompletionService.GetChatMessageContentAsync($"Este é um chatbot. Foi feita a pergunta para o usuario: ({this.GetCurrentQuestion()}) e ele digitou: ({resposta})? Ele respondeu a pergunta com a informação solicitada, ou ele está pedindo para sair do chat? Responda com (SIM = Ele quer sair ou não quer responder) ou (NÃO = Ele responde a pergunta de maneira coerente) e indique o motivo.").Result; + return resp.Content.Contains("SIM") ? "ABORTAR" : ""; + } + + public bool HasNextQuestion() + { + return _indiceAtual < _questions.Count; + } + } +} diff --git a/Services/Bot/Structs/ChatBot.cs~RFf86c93a.TMP b/Services/Bot/Structs/ChatBot.cs~RFf86c93a.TMP new file mode 100644 index 0000000..0957797 --- /dev/null +++ b/Services/Bot/Structs/ChatBot.cs~RFf86c93a.TMP @@ -0,0 +1,54 @@ +namespace ChatApi.Services.Bot.Structs +{ + public class ChatBot + { + private readonly List _Questions; + private readonly ChatPersistence _persistence; + private readonly Dictionary _answers; + private readonly Guid _usuarioId; + private int _indiceAtual; + + public ChatBot(ChatPersistence persistence, Guid usuarioId) + { + _Questions = new List(); + _answers = new Dictionary(); + _indiceAtual = 0; + _persistence = persistence; + _usuarioId = usuarioId; + + var estado = _persistence.GetState(_usuarioId); + if (estado != null) + { + _indiceAtual = estado.IndicePerguntaAtual; + _answers = estado.Respostas; + } + else + { + _indiceAtual = 0; + } + } + + public void AddQuestion(Question Question) + { + _Questions.Add(Question); + } + + public void Iniciar() + { + while (_indiceAtual < _Questions.Count) + { + var QuestionAtual = _Questions[_indiceAtual]; + if (QuestionAtual.TryToAnswer(out _)) + { + _indiceAtual++; + } + else + { + _indiceAtual = 0; // Reinicia o fluxo + } + } + + Console.WriteLine("Obrigado por responder a todas as perguntas!"); + } + } +} diff --git a/Services/Bot/Structs/ChatBot.cs~RFf89467e.TMP b/Services/Bot/Structs/ChatBot.cs~RFf89467e.TMP new file mode 100644 index 0000000..6c4eb52 --- /dev/null +++ b/Services/Bot/Structs/ChatBot.cs~RFf89467e.TMP @@ -0,0 +1,75 @@ +namespace ChatApi.Services.Bot.Structs +{ + public class ChatBot + { + private readonly List _questions; + private readonly ChatPersistence _persistence; + private readonly Dictionary _answers; + private readonly Guid _usuarioId; + private int _indiceAtual; + + public ChatBot(ChatPersistence persistence, Guid usuarioId) + { + _questions = new List(); + _answers = new Dictionary(); + _indiceAtual = 0; + _persistence = persistence; + _usuarioId = usuarioId; + + var estado = _persistence.GetState(_usuarioId); + if (estado != null) + { + _indiceAtual = estado.IndicePerguntaAtual; + _answers = estado.Respostas; + } + else + { + _indiceAtual = 0; + } + } + + public void AddQuestion(Question Question) + { + _questions.Add(Question); + } + + public bool InformarResposta(string resposta) + { + var perguntaAtual = _questions[_indiceAtual]; + if (perguntaAtual.Validator(resposta)) + { + _answers[_indiceAtual] = resposta; + _indiceAtual++; + + _persistence.SaveState(new ChatState + { + UsuarioId = _usuarioId, + IndicePerguntaAtual = _indiceAtual, + Respostas = new Dictionary(_answers) + }); + + return true; + } + + Console.WriteLine("Resposta inválida. Tente novamente."); + return false; + } + public void Iniciar() + { + while (_indiceAtual < _questions.Count) + { + var QuestionAtual = _questions[_indiceAtual]; + if (QuestionAtual.TryToAnswer(out _)) + { + _indiceAtual++; + } + else + { + _indiceAtual = 0; // Reinicia o fluxo + } + } + + Console.WriteLine("Obrigado por responder a todas as perguntas!"); + } + } +} diff --git a/Services/Bot/Structs/ChatPersistence.cs b/Services/Bot/Structs/ChatPersistence.cs new file mode 100644 index 0000000..ee7d554 --- /dev/null +++ b/Services/Bot/Structs/ChatPersistence.cs @@ -0,0 +1,45 @@ +using ChatApi.Services.Classifier; +using MongoDB.Driver; + +namespace ChatApi.Services.Bot.Structs +{ + public class ChatPersistence + { + private readonly IMongoCollection _collection; + private string _botName; + + private ChatPersistence(DomvsDatabaseSettings config) + { + var client = new MongoClient(config.ConnectionString); + var database = client.GetDatabase(config.DatabaseName); + _collection = database.GetCollection(config.ChatBotRHCollectionName); + } + + public static ChatPersistence Create(DomvsDatabaseSettings config) + { + return new ChatPersistence(config); + } + + public ChatPersistence SetBotName(string botName) + { + _botName = botName; + return this; + } + + public ChatState GetState(string usuarioId) + { + return _collection.Find(x => x.UsuarioId == usuarioId).FirstOrDefault(); + } + + public void DeleteState(string usuarioId) + { + _collection.DeleteOne(x => x.UsuarioId == usuarioId); + } + + public void SaveState(ChatState estado) + { + var filtro = Builders.Filter.Eq(x => x.UsuarioId, estado.UsuarioId); + _collection.ReplaceOne(filtro, estado, new ReplaceOptions { IsUpsert = true }); + } + } +} diff --git a/Services/Bot/Structs/ChatState.cs b/Services/Bot/Structs/ChatState.cs new file mode 100644 index 0000000..919b983 --- /dev/null +++ b/Services/Bot/Structs/ChatState.cs @@ -0,0 +1,20 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson.Serialization.Options; + +namespace ChatApi.Services.Bot.Structs +{ + public class ChatState + { + [BsonId] + [BsonElement("_id")] + public string Id { get; set; } + public string UsuarioId { get; set; } + public int IndicePerguntaAtual { get; set; } + public int TentativasPerguntaAtual { get; set; } + + [BsonDictionaryOptions(DictionaryRepresentation.ArrayOfArrays)] + public Dictionary Respostas { get; set; } + public Dictionary DadosConhecidos { get; set; } + } +} diff --git a/Services/Bot/Structs/IActionCall.cs b/Services/Bot/Structs/IActionCall.cs new file mode 100644 index 0000000..bef2fc3 --- /dev/null +++ b/Services/Bot/Structs/IActionCall.cs @@ -0,0 +1,10 @@ +using ChatApi.Infra; + +namespace ChatApi.Services.Bot.Structs +{ + public interface IActionCall + { + Task Populate(Dictionary? knowParameters, Dictionary answers); + Task> Call(); + } +} diff --git a/Services/Bot/Structs/Question.cs b/Services/Bot/Structs/Question.cs new file mode 100644 index 0000000..4156464 --- /dev/null +++ b/Services/Bot/Structs/Question.cs @@ -0,0 +1,42 @@ +namespace ChatApi.Services.Bot.Structs +{ + public class Question + { + public string Text { get; } + public Func Validator { get; } + public int CountTry { get; set; } + public Func, bool> Condition { get; } + + private const int MaxTentativas = 2; + + public Question(string texto, Func validador, Func, bool> condition=null) + { + Text = texto; + Validator = validador; + CountTry = 0; + Condition = condition; + } + + public bool MustShow(Dictionary respostasAnteriores) + { + return Condition == null || Condition(respostasAnteriores); + } + + public string TryToAnswer(string resposta) + { + if (Validator(resposta)) + { + return ""; + } + + CountTry++; + if (CountTry >= MaxTentativas) + { + CountTry = 0; + return "Não entendi sua resposta. Vamos reiniciar."; + } + + return "Resposta inválida. Tente novamente."; + } + } +} diff --git a/Services/Bot/Structs/e4zbjmgl.vwg~ b/Services/Bot/Structs/e4zbjmgl.vwg~ new file mode 100644 index 0000000..dd7b2a6 --- /dev/null +++ b/Services/Bot/Structs/e4zbjmgl.vwg~ @@ -0,0 +1,137 @@ +using Amazon.Auth.AccessControlPolicy; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace ChatApi.Services.Bot.Structs +{ + public class ChatBot + { + private readonly List _questions; + private readonly ChatPersistence _persistence; + private readonly Dictionary _knowParameters; + private readonly Dictionary _answers; + private readonly IChatCompletionService _chatCompletionService; + private readonly IActionCall _actionCall; + private readonly string _usuarioId; + private int _indiceAtual; + + public ChatBot(ChatPersistence persistence, string usuarioId, IChatCompletionService chatCompletionService, IActionCall actionCall) + { + _chatCompletionService = chatCompletionService; + _actionCall = actionCall; + _questions = new List(); + _answers = new Dictionary(); + _knowParameters = new Dictionary(); + _indiceAtual = 0; + _persistence = persistence; + _usuarioId = usuarioId; + + var estado = _persistence.GetState(_usuarioId); + if (estado != null) + { + _indiceAtual = estado.IndicePerguntaAtual; + _answers = estado.Respostas; + } + else + { + _indiceAtual = 0; + } + } + + public void SetParameter(string key, string value) + { + _knowParameters[key] = value; + } + + public void AddQuestion(Question Question) + { + _questions.Add(Question); + } + + public string SetAnswer(string resposta) + { + if (!this.HasNextQuestion()) return ""; + + var state = _persistence.GetState(_usuarioId); + if (state != null) + { + _questions[_indiceAtual].CountTry = state.TentativasPerguntaAtual; + } + + var perguntaAtual = _questions[_indiceAtual]; + var testByChat = TestAnswerByChat(resposta); + var abort = TestIfWantToAbort(resposta); + var validResp = perguntaAtual.TryToAnswer(resposta); + + if (string.IsNullOrEmpty(validResp) && !abort.Contains("ABORTAR") && perguntaAtual.Validator(resposta) && testByChat.Contains("SIM")) + { + _answers[_indiceAtual] = resposta; + _indiceAtual++; + _persistence.SaveState(new ChatState + { + Id = _usuarioId, + UsuarioId = _usuarioId, + IndicePerguntaAtual = _indiceAtual, + TentativasPerguntaAtual = _questions[_indiceAtual].CountTry, + Respostas = new Dictionary(_answers) + }); + + return ""; + } + else if (validResp.Contains("reiniciar") || abort.Contains("ABORTAR")) + { + _persistence.DeleteState(_usuarioId); + return "REINICIAR"; + } + + _persistence.SaveState(new ChatState + { + Id = _usuarioId, + UsuarioId = _usuarioId, + IndicePerguntaAtual = _indiceAtual, + TentativasPerguntaAtual = _questions[_indiceAtual].CountTry, + Respostas = new Dictionary(_answers) + }); + + return $"Resposta inválida. Tente novamente. {(testByChat.Contains("NÃO") ? "Motivo: " + testByChat.Replace("NÂO", "") : "")} \n\n {perguntaAtual.Text}"; + } + + public string GetCurrentQuestion() => _questions[_indiceAtual].Text; + public async Task GetNextQuestion() + { + while (_indiceAtual < _questions.Count) + { + var perguntaAtual = _questions[_indiceAtual]; + if (perguntaAtual.MustShow(_answers)) + { + return perguntaAtual.Text; + } + _indiceAtual++; + } + + var resp = await _actionCall.Populate(_knowParameters, _answers); + if (!resp.Success) return "Obrigado por responder a nossas perguntas!"; + + var result = await _actionCall.Call(); + + return result.Success ? result.Value : "Obrigado por responder a nossas perguntas!"; + } + + public string TestAnswerByChat(string resposta) + { + var resp = _chatCompletionService.GetChatMessageContentAsync($"Por favor, responda com SIM/NÂO e o motivo para eu saber se a pergunta: ({this.GetCurrentQuestion()}) foi respondida pelo usuário quando ele digitou: ({resposta})? É um chatbot, por isso preciso saber apenas se a resposta faz sentido e/ou tem o formato certo.").Result; + return resp.Content; + } + + public string TestIfWantToAbort(string resposta) + { + if (_indiceAtual==0) return ""; + var resp = _chatCompletionService.GetChatMessageContentAsync($"Este é um chatbot. Foi feita a pergunta para o usuario: ({this.GetCurrentQuestion()}) e ele digitou: ({resposta})? Ele respondeu a pergunta com a informação solicitada, ou ele está pedindo para sair do chat? Responda com (SIM = Ele quer sair ou não quer responder) ou (NÃO = Ele responde a pergunta de maneira coerente) e indique o motivo.").Result; + return resp.Content.Contains("SIM") ? "ABORTAR" : ""; + } + + public bool HasNextQuestion() + { + return _indiceAtual < _questions.Count; + } + } +} diff --git a/Services/Bot/Structs/v0pubwmg.zkp~ b/Services/Bot/Structs/v0pubwmg.zkp~ new file mode 100644 index 0000000..31225ba --- /dev/null +++ b/Services/Bot/Structs/v0pubwmg.zkp~ @@ -0,0 +1,148 @@ +using Amazon.Auth.AccessControlPolicy; +using ChatApi.Models; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace ChatApi.Services.Bot.Structs +{ + public class ChatBot + { + private readonly List _questions; + private readonly ChatPersistence _persistence; + private readonly Dictionary _answers; + private readonly IChatCompletionService _chatCompletionService; + private readonly IActionCall _actionCall; + private readonly string _usuarioId; + private readonly UserData _userData; + private Dictionary _knowParameters; + private int _indiceAtual; + + public ChatBot(ChatPersistence persistence, string usuarioId, UserData userData, IChatCompletionService chatCompletionService, IActionCall actionCall) + { + _chatCompletionService = chatCompletionService; + _actionCall = actionCall; + _questions = new List(); + _answers = new Dictionary(); + _knowParameters = new Dictionary(); + _indiceAtual = 0; + _persistence = persistence; + _usuarioId = usuarioId; + _userData = userData; + var estado = _persistence.GetState(_usuarioId); + if (estado != null) + { + _indiceAtual = estado.IndicePerguntaAtual; + _answers = estado.Respostas; + } + else + { + _indiceAtual = 0; + } + } + + public void SetParameter(string key, string value) + { + _knowParameters[key] = value; + } + + public void AddQuestion(Question Question) + { + _questions.Add(Question); + } + + public string SetAnswer(string resposta) + { + if (!this.HasNextQuestion()) return ""; + + var state = _persistence.GetState(_usuarioId); + if (state != null) + { + _questions[_indiceAtual].CountTry = state.TentativasPerguntaAtual; + } + else + { + _knowParameters = new Dictionary() + { + { "Nome", _userData.Name }, + { "Email", _userData.Email } + }; + } + + var perguntaAtual = _questions[_indiceAtual]; + var testByChat = TestAnswerByChat(resposta); + var abort = TestIfWantToAbort(resposta); + var validResp = perguntaAtual.TryToAnswer(resposta); + + if (string.IsNullOrEmpty(validResp) && !abort.Contains("ABORTAR") && perguntaAtual.Validator(resposta) && testByChat.Contains("SIM")) + { + _answers[_indiceAtual] = resposta; + _indiceAtual++; + _persistence.SaveState(new ChatState + { + Id = _usuarioId, + UsuarioId = _usuarioId, + IndicePerguntaAtual = _indiceAtual, + TentativasPerguntaAtual = _questions[_indiceAtual].CountTry, + Respostas = new Dictionary(_answers) + }); + + return ""; + } + else if (validResp.Contains("reiniciar") || abort.Contains("ABORTAR")) + { + _persistence.DeleteState(_usuarioId); + return "REINICIAR"; + } + + _persistence.SaveState(new ChatState + { + Id = _usuarioId, + UsuarioId = _usuarioId, + IndicePerguntaAtual = _indiceAtual, + TentativasPerguntaAtual = _questions[_indiceAtual].CountTry, + DadosConhecidos = _knowParameters, + Respostas = new Dictionary(_answers) + }); + + return $"Resposta inválida. Tente novamente. {(testByChat.Contains("NÃO") ? "Motivo: " + testByChat.Replace("NÂO", "") : "")} \n\n {perguntaAtual.Text}"; + } + + public string GetCurrentQuestion() => _questions[_indiceAtual].Text; + public async Task GetNextQuestion() + { + while (_indiceAtual < _questions.Count) + { + var perguntaAtual = _questions[_indiceAtual]; + if (perguntaAtual.MustShow(_answers)) + { + return perguntaAtual.Text; + } + _indiceAtual++; + } + + var resp = await _actionCall.Populate(_knowParameters, _answers); + if (!resp.Success) return "Obrigado por responder a nossas perguntas!"; + + var result = await _actionCall.Call(); + + return result.Success ? result. : "Obrigado por responder a nossas perguntas!"; + } + + public string TestAnswerByChat(string resposta) + { + var resp = _chatCompletionService.GetChatMessageContentAsync($"Por favor, responda com SIM/NÂO e o motivo para eu saber se a pergunta: ({this.GetCurrentQuestion()}) foi respondida pelo usuário quando ele digitou: ({resposta})? É um chatbot, por isso preciso saber apenas se a resposta faz sentido e/ou tem o formato certo.").Result; + return resp.Content; + } + + public string TestIfWantToAbort(string resposta) + { + if (_indiceAtual==0) return ""; + var resp = _chatCompletionService.GetChatMessageContentAsync($"Este é um chatbot. Foi feita a pergunta para o usuario: ({this.GetCurrentQuestion()}) e ele digitou: ({resposta})? Ele respondeu a pergunta com a informação solicitada, ou ele está pedindo para sair do chat? Responda com (SIM = Ele quer sair ou não quer responder) ou (NÃO = Ele responde a pergunta de maneira coerente) e indique o motivo.").Result; + return resp.Content.Contains("SIM") ? "ABORTAR" : ""; + } + + public bool HasNextQuestion() + { + return _indiceAtual < _questions.Count; + } + } +} diff --git a/Services/Bot/tdpgs1ms.1ky~ b/Services/Bot/tdpgs1ms.1ky~ new file mode 100644 index 0000000..a4875ed --- /dev/null +++ b/Services/Bot/tdpgs1ms.1ky~ @@ -0,0 +1,64 @@ +using ChatApi.Services.Bot.Structs; + +namespace ChatApi.Services.Bot +{ + public class ChatBotRHCall + { + private readonly ChatBot _chatbot; + + Dictionary _opcoesSolicitação = + new Dictionary { + { "Inclusão/Exclusão Alura", "Inclusão/Exclusão Alura"}, + { "Inclusão/Exclusão Auxílio Idioma", "Inclusão/Exclusão Auxílio Idioma"}, + { "Inclusão/Exclusão Plano Odonto", "Inclusão/Exclusão Plano Odonto"}, + //{ "Envio de Atestado", "Envio de Atestado"}, + //{ "Envio de Reembolso Idioma", "Envio de Reembolso Idioma"}, + //{ "Afastamento e Licenças", "Afastamento e Licenças"}, + { "Dúvidas Plano de Saúde", "Dúvidas Plano de Saúde"}, + { "Dúvidas Folha de Pagamento", "Dúvidas Folha de Pagamento"}, + { "Ajuste Cargo/Salário", "Ajuste Cargo/Salário"}, + { "Feedbacks, reclamações", "1 - Feedbacks, reclamações"}, + { "Feedz - Dúvida 1:1", "Feedz - Dúvida 1:1"}, + { "Feedz - Dúvida Feedback", "Feedz - Dúvida Feedback"}, + { "Feedz - Cadastro de Objetivo", "Feedz - Cadastro de Objetivo"}, + { "Feedz - Erro Cadastro", "Feedz - Erro Cadastro"}, + { "Feedz - Outros", "Feedz - Outros"}, + { "Ciclo de Performance", "Ciclo de Performance"}, + { "Declaração Bolsa Faculdade", "Declaração Bolsa Faculdade"}, + { "Inclusão/Exclusão Programa de Especialização", "Inclusão/Exclusão Programa de Especialização"}, + //{ "Envio de Reembolso Especialização", "Envio de Reembolso Especialização"}, + { "DayOff", "DayOff"} + }; + + public ChatBotRHCall(ChatBot chatbot) + { + _chatbot = chatbot; + AddNumbersBeforShow(_opcoesSolicitação); + + _chatbot.AddQuestion(new Question( + "Qual seu número de celular (com WhatsApp)?", + resposta => !string.IsNullOrEmpty(resposta) && resposta.Length >= 10)); + + _chatbot.AddQuestion(new Question( + "Qual o tipo de solicitação (1 para Reembolso de Idioma, 2 para Reembolso Alura, 3 para Dúvidas)?", + resposta => resposta == "1" || resposta == "2" || resposta == "3")); + + _chatbot.AddQuestion(new Question( + "Texto/Descrição da solicitação (em caso de dúvidas)", + resposta => !string.IsNullOrEmpty(resposta))); + + _chatbot.AddQuestion(new Question( + "Tudo bem? Posso enviar sua solicitação?", + resposta => !string.IsNullOrEmpty(resposta))); + } + + private void AddNumbersBeforShow(Dictionary dict) + { + int optionNumber = 1; + foreach (var key in dict.Keys) + { + dict[key] = $"{optionNumber} - {dict[key]}"; + } + } + } +} diff --git a/Services/Classifier/ClassifierPersistence.cs b/Services/Classifier/ClassifierPersistence.cs new file mode 100644 index 0000000..631e0b3 --- /dev/null +++ b/Services/Classifier/ClassifierPersistence.cs @@ -0,0 +1,35 @@ +using ChatApi.Services.Bot.Structs; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace ChatApi.Services.Classifier +{ + public class ClassifierPersistence + { + private readonly IMongoCollection _collection; + + public ClassifierPersistence(IOptions config) + { + var client = new MongoClient(config.Value.ConnectionString); + var database = client.GetDatabase(config.Value.DatabaseName); + _collection = database.GetCollection(config.Value.ClassifierCollectionName); + } + + public ClassifierSate GetState(string usuarioId) + { + return _collection.Find(x => x.Id == usuarioId).FirstOrDefault(); + } + + public void DeleteState(string usuarioId) + { + _collection.DeleteOne(x => x.UsuarioId == usuarioId); + } + + public void SaveState(ClassifierSate estado) + { + estado.Id = estado.UsuarioId; + var filtro = Builders.Filter.Eq(x => x.Id, estado.UsuarioId); + _collection.ReplaceOne(filtro, estado, new ReplaceOptions { IsUpsert = true }); + } + } +} diff --git a/Services/Classifier/ClassifierSate.cs b/Services/Classifier/ClassifierSate.cs new file mode 100644 index 0000000..1968dda --- /dev/null +++ b/Services/Classifier/ClassifierSate.cs @@ -0,0 +1,17 @@ +using ChatApi.Services.ClassifyHandlers; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace ChatApi.Services.Classifier +{ + public class ClassifierSate + { + [BsonId] + [BsonElement("_id")] + public string Id { get; set; } + public string UsuarioId { get; set; } + public EnumClassification Classification { get; set; } + public DateTime DhInicio { get; set; } + public EnumClassificationType ClassificationType { get; set; } + } +} diff --git a/Services/Classifier/ClassifierSate.cs~RF14991194.TMP b/Services/Classifier/ClassifierSate.cs~RF14991194.TMP new file mode 100644 index 0000000..cfdf7c0 --- /dev/null +++ b/Services/Classifier/ClassifierSate.cs~RF14991194.TMP @@ -0,0 +1,6 @@ +namespace ChatApi.Services.Classifier +{ + public class ClassifierSate + { + } +} diff --git a/Services/ClassifyHandlers/AClassifyHandler.cs b/Services/ClassifyHandlers/AClassifyHandler.cs new file mode 100644 index 0000000..1ea3d7a --- /dev/null +++ b/Services/ClassifyHandlers/AClassifyHandler.cs @@ -0,0 +1,50 @@ +namespace ChatApi.Services.ClassifyHandlers +{ + public enum EnumClassificationType + { + Stay, + Free + } + + public class AHandlerClassify : IHandlerClassify + { + private IHandlerClassify _nextHandlerClassify; + protected TextFilter _textFilter; + private EnumClassificationType _type = EnumClassificationType.Free; + + public EnumClassificationType Type + { + get + { + return _type; + } + protected set + { + _type = EnumClassificationType.Stay; + } + } + + public int StayInMinutes { get; set; } + public EnumClassification MyClassification { get; set; } + + public AHandlerClassify() + { + _textFilter = new TextFilter(); + } + + public IHandlerClassify SetNext(IHandlerClassify handler) + { + _nextHandlerClassify = handler; + return handler; + } + + public virtual EnumClassification Handle(string request) + { + if (_nextHandlerClassify != null) + { + return _nextHandlerClassify.Handle(request); + } + return EnumClassification.CantDetect; + } + } +} diff --git a/Services/ClassifyHandlers/ClassifyBotRHCall.cs b/Services/ClassifyHandlers/ClassifyBotRHCall.cs new file mode 100644 index 0000000..6111335 --- /dev/null +++ b/Services/ClassifyHandlers/ClassifyBotRHCall.cs @@ -0,0 +1,30 @@ +namespace ChatApi.Services.ClassifyHandlers +{ + public class ClassifyBotRHCall : AHandlerClassify + { + public ClassifyBotRHCall() + { + this.MyClassification = EnumClassification.BotRHCall; + this.Type = EnumClassificationType.Stay; + this.StayInMinutes = 240; + } + + public override EnumClassification Handle(string request) + { + var companyKeywords = new List { "solicitação", "solicitações", "pedido", "dúvida", "dúvidas", "alura", "auxilio idioma", "plano odonto", "envio atestado", "reembolso idioma", "afastamento", "Agility as a Service", "agilidade", + "plano de saúde", "folha de pagamento", "feedbacks", "reclamações", "1:1", "feedback", "Erro de cadsatro", "performance", + "bolsa faculdade", "especialização", "reembolso de especialização", "dayoff", "aws", "cadastro", "dúvidas" + }; + + var questionLower = base._textFilter.ToLowerAndWithoutAccents(request); + + if (questionLower.Contains(_textFilter.ToLowerAndWithoutAccents(" rh")) && + companyKeywords.Any(keyword => questionLower.Contains(_textFilter.ToLowerAndWithoutAccents(keyword)))) + { + return this.MyClassification; + } + + return base.Handle(request); + } + } +} diff --git a/Services/ClassifyHandlers/ClassifyChat.cs b/Services/ClassifyHandlers/ClassifyChat.cs new file mode 100644 index 0000000..1692a9d --- /dev/null +++ b/Services/ClassifyHandlers/ClassifyChat.cs @@ -0,0 +1,10 @@ +namespace ChatApi.Services.ClassifyHandlers +{ + public class ClassifyChat : AHandlerClassify + { + public override EnumClassification Handle(string request) + { + return EnumClassification.Chat; + } + } +} diff --git a/Services/ClassifyHandlers/ClassifyCompany.cs b/Services/ClassifyHandlers/ClassifyCompany.cs new file mode 100644 index 0000000..b6ad4a7 --- /dev/null +++ b/Services/ClassifyHandlers/ClassifyCompany.cs @@ -0,0 +1,25 @@ +namespace ChatApi.Services.ClassifyHandlers +{ + public class ClassifyCompany : AHandlerClassify + { + public ClassifyCompany() + { + this.MyClassification = EnumClassification.Company; + } + + public override EnumClassification Handle(string request) + { + var companyKeywords = new List { "Domvs", "DomvsiT", "squad gerenciada", "squad híbrida", "alocação", "Professional Service", "solução", "Soluções", "Agility as a Service", "agilidade", + "DDC", "Design Diamond Conception", "Bussiness Process Optmization", "Otimização de processos", "Soluções em Cloud", "Delphix", "UX", "Squad" }; + + var questionLower = base._textFilter.ToLowerAndWithoutAccents(request); + + if (companyKeywords.Any(keyword => questionLower.Contains(_textFilter.ToLowerAndWithoutAccents(keyword)))) + { + return this.MyClassification; + } + + return base.Handle(request); + } + } +} diff --git a/Services/ClassifyHandlers/ClassifyEstimate.cs b/Services/ClassifyHandlers/ClassifyEstimate.cs new file mode 100644 index 0000000..d5fc054 --- /dev/null +++ b/Services/ClassifyHandlers/ClassifyEstimate.cs @@ -0,0 +1,20 @@ +namespace ChatApi.Services.ClassifyHandlers +{ + public class ClassifyEstimate : AHandlerClassify + { + public override EnumClassification Handle(string request) + { + var companyKeywords = new List { "Domvs", "DomvsiT", "squad gerenciada", "squad híbrida", "alocação", "Professional Service", "solução", "Soluções", "Agility as a Service", "agilidade", + "DDC", "Design Diamond Conception", "Bussiness Process Optmization", "Otimização de processos", "Soluções em Cloud", "Delphix", "UX", "Squad" }; + + var questionLower = base._textFilter.ToLowerAndWithoutAccents(request); + + if (companyKeywords.Any(keyword => questionLower.Contains(_textFilter.ToLowerAndWithoutAccents(keyword)))) + { + return EnumClassification.Company; + } + + return base.Handle(request); + } + } +} diff --git a/Services/ClassifyHandlers/IHandlerClassify.cs b/Services/ClassifyHandlers/IHandlerClassify.cs new file mode 100644 index 0000000..ba2f104 --- /dev/null +++ b/Services/ClassifyHandlers/IHandlerClassify.cs @@ -0,0 +1,11 @@ +using System.Reflection.Metadata; + +namespace ChatApi.Services.ClassifyHandlers +{ + public interface IHandlerClassify + { + IHandlerClassify SetNext(IHandlerClassify handler); + + EnumClassification Handle(string request); + } +} diff --git a/Services/ResponseService/IResponseService.cs b/Services/ResponseService/IResponseService.cs new file mode 100644 index 0000000..bbc4fcd --- /dev/null +++ b/Services/ResponseService/IResponseService.cs @@ -0,0 +1,10 @@ +using ChatApi.Models; + +namespace ChatApi.Services.ResponseService +{ + public interface IResponseService + { + EnumClassification Classification { get; } + Task GetResponse(HttpContext context, UserData userData, string sessionId, string question); + } +} diff --git a/Services/ResponseService/ResponseBotRHService.cs b/Services/ResponseService/ResponseBotRHService.cs new file mode 100644 index 0000000..fbaeb24 --- /dev/null +++ b/Services/ResponseService/ResponseBotRHService.cs @@ -0,0 +1,76 @@ + +using ChatApi.Models; +using ChatApi.Services.Bot; +using ChatApi.Services.Bot.Structs; +using ChatApi.Services.Classifier; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Embeddings; +using OllamaSharp.Models.Chat; + +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace ChatApi.Services.ResponseService +{ + public class ResponseBotRHService : IResponseService + { + private readonly ChatHistoryService _chatHistoryService; + private readonly TextFilter _textFilter; + private readonly ChatBotRHCall _chatBotCall; + private readonly ClassifierPersistence _classifierPersistence; + + public ResponseBotRHService( + ChatHistoryService chatHistoryService, + TextFilter textFilter, + ChatBotRHCall chatBotCall, + ClassifierPersistence classifierPersistence) + { + _chatHistoryService = chatHistoryService; + _textFilter = textFilter; + _chatBotCall = chatBotCall; + _classifierPersistence = classifierPersistence; + } + public EnumClassification Classification => EnumClassification.BotRHCall; + + public async Task GetResponse(HttpContext context, UserData userData, string sessionId, string question) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + + SessionIdStore sessionIdStore = new SessionIdStore(context); + ChatHistory history = _chatHistoryService.GetSumarizer(sessionId); + _chatBotCall.UserData = userData; + var resp = _chatBotCall.SetAnswer(sessionId, question); + var resposta = ""; + if (string.IsNullOrEmpty(resp) && resp!="REINICIAR") + { + resposta = await _chatBotCall.GetNextQuestion(); + history.AddUserMessage(question); + + history.AddMessage(AuthorRole.Assistant, resposta ?? ""); + + _chatHistoryService.UpdateHistory(sessionId, history); + + if (!_chatBotCall.HasNextQuestion()) + { + _classifierPersistence.DeleteState(sessionId); + } + } + else + { + if (resp == "REINICIAR") + { + _classifierPersistence.DeleteState(sessionId); + resp = "Ok! Parece que você não quer continuar e/ou eu não entendi sua resposta. Tudo bem! Se quiser, tente novamente depois..."; + } + + resposta = resp; + } + + stopWatch.Stop(); + return $"{resposta ?? ""}\n\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s"; + } + } +} + +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/Services/ResponseService/ResponseChatService.cs b/Services/ResponseService/ResponseChatService.cs new file mode 100644 index 0000000..59f9b27 --- /dev/null +++ b/Services/ResponseService/ResponseChatService.cs @@ -0,0 +1,42 @@ + +using ChatApi.Models; +using Microsoft.SemanticKernel.ChatCompletion; +using OllamaSharp.Models.Chat; +using System.Diagnostics; + +namespace ChatApi.Services.ResponseService +{ + public class ResponseChatService : IResponseService + { + private readonly ChatHistoryService _chatHistoryService; + private readonly IChatCompletionService _chatCompletionService; + + public ResponseChatService( + ChatHistoryService chatHistoryService, + IChatCompletionService chatCompletionService) + { + this._chatHistoryService = chatHistoryService; + this._chatCompletionService = chatCompletionService; + } + public EnumClassification Classification => EnumClassification.Chat; + + public async Task GetResponse(HttpContext context, UserData userData, string sessionId, string question) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + + stopWatch.Start(); + + SessionIdStore sessionIdStore = new SessionIdStore(context); + ChatHistory history = _chatHistoryService.Get(sessionId); + + history.AddUserMessage(question); + var response = await _chatCompletionService.GetChatMessageContentAsync(history); + history.AddMessage(response.Role, response.Content ?? ""); + + _chatHistoryService.UpdateHistory(sessionId, history); + + stopWatch.Stop(); + return $"{response.Content ?? ""}\n\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s"; + } + } +} diff --git a/Services/ResponseService/ResponseCompanyService.cs b/Services/ResponseService/ResponseCompanyService.cs new file mode 100644 index 0000000..db166ab --- /dev/null +++ b/Services/ResponseService/ResponseCompanyService.cs @@ -0,0 +1,99 @@ + +using ChatApi.Models; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Embeddings; +using OllamaSharp.Models.Chat; + +#pragma warning disable SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +namespace ChatApi.Services.ResponseService +{ + public class ResponseCompanyService : IResponseService + { + private readonly ChatHistoryService _chatHistoryService; + private readonly Kernel _kernel; + private readonly TextFilter _textFilter; + private readonly SharepointDomvsService _sharepointDomvsService; + private readonly IChatCompletionService _chatCompletionService; + + public ResponseCompanyService( + ChatHistoryService chatHistoryService, + Kernel kernel, + TextFilter textFilter, + SharepointDomvsService sharepointDomvsService, + IChatCompletionService chatCompletionService) + { + this._chatHistoryService = chatHistoryService; + this._kernel = kernel; + this._textFilter = textFilter; + this._sharepointDomvsService = sharepointDomvsService; + this._chatCompletionService = chatCompletionService; + } + public EnumClassification Classification => EnumClassification.Company; + + public async Task GetResponse(HttpContext context, UserData userData, string sessionId, string question) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + + SessionIdStore sessionIdStore = new SessionIdStore(context); + + var resposta = await BuscarTextoRelacionado(question); + question = "Para responder à pergunta: \"" + question + "\" por favor, gere um resumo com 3 linhas baseado exclusivamente no texto: \"" + resposta + "\""; + ChatHistory history = _chatHistoryService.GetSumarizer(sessionId); + + history.AddUserMessage(question); + + var response = await _chatCompletionService.GetChatMessageContentAsync(history); + history.AddMessage(response.Role, response.Content ?? ""); + + _chatHistoryService.UpdateHistory(sessionId, history); + + stopWatch.Stop(); + return $"{response.Content ?? ""}\n\nTempo: {stopWatch.ElapsedMilliseconds / 1000}s"; + + } + + async Task BuscarTextoRelacionado(string pergunta) + { + var embeddingService = _kernel.GetRequiredService(); + var embeddingPergunta = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(pergunta)); + var embeddingArrayPergunta = embeddingPergunta.ToArray().Select(e => (double)e).ToArray(); + + var textos = await _sharepointDomvsService.GetAsync(); + + TextoComEmbedding melhorTexto = null; + double melhorSimilaridade = -1.0; + + foreach (var texto in textos) + { + double similaridade = CalcularSimilaridadeCoseno(embeddingArrayPergunta, texto.Embedding); + if (similaridade > melhorSimilaridade) + { + melhorSimilaridade = similaridade; + melhorTexto = texto; + } + } + + // 4. Retornar o conteúdo mais similar, ou uma mensagem padrão + return melhorTexto != null ? melhorTexto.Conteudo : "Não encontrei uma resposta adequada."; + } + + double CalcularSimilaridadeCoseno(double[] embedding1, double[] embedding2) + { + double dotProduct = 0.0; + double normA = 0.0; + double normB = 0.0; + for (int i = 0; i < embedding1.Length; i++) + { + dotProduct += embedding1[i] * embedding2[i]; + normA += Math.Pow(embedding1[i], 2); + normB += Math.Pow(embedding2[i], 2); + } + return dotProduct / (Math.Sqrt(normA) * Math.Sqrt(normB)); + } + } +} + +#pragma warning restore SKEXP0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. diff --git a/Services/ResponseService/ResponseFactory.cs b/Services/ResponseService/ResponseFactory.cs new file mode 100644 index 0000000..f1fd60a --- /dev/null +++ b/Services/ResponseService/ResponseFactory.cs @@ -0,0 +1,18 @@ +using System.Net.NetworkInformation; + +namespace ChatApi.Services.ResponseService +{ + public class ResponseFactory + { + private readonly IEnumerable _responseServices; + public ResponseFactory(IEnumerable responseService) + { + _responseServices = responseService; + } + + public IResponseService? GetService(EnumClassification classification) + { + return _responseServices.FirstOrDefault(w => w.Classification == classification); + } + } +} diff --git a/Services/TextClassifier.cs b/Services/TextClassifier.cs new file mode 100644 index 0000000..0f7a925 --- /dev/null +++ b/Services/TextClassifier.cs @@ -0,0 +1,60 @@ +using ChatApi.Services.Classifier; +using ChatApi.Services.ClassifyHandlers; + +namespace ChatApi.Services +{ + public enum EnumClassification + { + Company=0, + Estimate=1, + BotRHCall = 2, + Chat = 3, + CantDetect=4 + } + + /// + /// enum + /// + public class TextClassifier + { + private readonly TextFilter _textFilter; + private readonly ClassifierPersistence _classifierPersistence; + + public TextClassifier(TextFilter textFilter, ClassifierPersistence classifierPersistence) + { + _textFilter = textFilter; + _classifierPersistence = classifierPersistence; + } + public async Task ClassifyQuestion(string sessionId, string question) + { + var state = _classifierPersistence.GetState(sessionId); + + if (state!=null && (state.ClassificationType == EnumClassificationType.Stay)) + { + return state.Classification; + } + else + { + var classifyCompany = new ClassifyCompany(); + var classifyChat = new ClassifyChat(); + var botChat = new ClassifyBotRHCall(); + + classifyCompany + .SetNext(botChat) + .SetNext(classifyChat); + + var classify = classifyCompany.Handle(question); + + _classifierPersistence.SaveState(new ClassifierSate + { + Id = sessionId, + UsuarioId = sessionId, + Classification = classify, + DhInicio = DateTime.Now, + ClassificationType = botChat.MyClassification == classify ? EnumClassificationType.Stay : EnumClassificationType.Free + }); + return classify; + } + } + } +} diff --git a/Services/TextFilter.cs b/Services/TextFilter.cs new file mode 100644 index 0000000..15a0594 --- /dev/null +++ b/Services/TextFilter.cs @@ -0,0 +1,33 @@ +using System.Globalization; +using System.Text; + +namespace ChatApi.Services +{ + public class TextFilter + { + public string ToLowerAndWithoutAccents(string text) + { + return RemoveDiacritics(text.ToLower()); + } + + public string RemoveDiacritics(string text) + { + var normalizedString = text.Normalize(NormalizationForm.FormD); + var stringBuilder = new StringBuilder(capacity: normalizedString.Length); + + for (int i = 0; i < normalizedString.Length; i++) + { + char c = normalizedString[i]; + var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c); + if (unicodeCategory != UnicodeCategory.NonSpacingMark) + { + stringBuilder.Append(c); + } + } + + return stringBuilder + .ToString() + .Normalize(NormalizationForm.FormC); + } + } +} diff --git a/SessionIdStore.cs b/SessionIdStore.cs new file mode 100644 index 0000000..fbd1b3a --- /dev/null +++ b/SessionIdStore.cs @@ -0,0 +1,32 @@ +namespace ChatApi +{ + public class SessionIdStore + { + HttpContext _context; + + public SessionIdStore(HttpContext httpContext) + { + _context = httpContext; + } + + public string GetSessionId() + { + var sessionId = _context.Request.Cookies["ChatSessionId"]; + + if (sessionId == null) + { + sessionId = Guid.NewGuid().ToString("N"); + _context.Response.Cookies.Append("ChatSessionId", sessionId, new CookieOptions + { + Expires = DateTimeOffset.Now.AddMonths(12), + HttpOnly = true, + SameSite = SameSiteMode.None, + Secure = true + }); + } + + + return sessionId; + } + } +} diff --git a/Settings/ChatRHSettings.cs b/Settings/ChatRHSettings.cs new file mode 100644 index 0000000..ca37ab1 --- /dev/null +++ b/Settings/ChatRHSettings.cs @@ -0,0 +1,8 @@ +namespace ChatApi.Settings +{ + public class ChatRHSettings + { + public string Url { get; set; } + public string Create { get; set; } + } +} diff --git a/SharepointDomvsService.cs b/SharepointDomvsService.cs new file mode 100644 index 0000000..e4956ca --- /dev/null +++ b/SharepointDomvsService.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace ChatApi +{ + public class SharepointDomvsService + { + private readonly IMongoCollection _booksCollection; + + public SharepointDomvsService( + IOptions bookStoreDatabaseSettings) + { + var mongoClient = new MongoClient( + bookStoreDatabaseSettings.Value.ConnectionString); + + var mongoDatabase = mongoClient.GetDatabase( + bookStoreDatabaseSettings.Value.DatabaseName); + + _booksCollection = mongoDatabase.GetCollection( + bookStoreDatabaseSettings.Value.SharepointCollectionName); + } + + public async Task> GetAsync() => + await _booksCollection.Find(_ => true).ToListAsync(); + + public async Task GetAsync(string id) => + await _booksCollection.Find(x => x.Id == id).FirstOrDefaultAsync(); + + public async Task CreateAsync(TextoComEmbedding newBook) => + await _booksCollection.InsertOneAsync(newBook); + + public async Task UpdateAsync(string id, TextoComEmbedding updatedBook) => + await _booksCollection.ReplaceOneAsync(x => x.Id == id, updatedBook); + + public async Task RemoveAsync(string id) => + await _booksCollection.DeleteOneAsync(x => x.Id == id); + } +} diff --git a/TextoComEmbedding.cs b/TextoComEmbedding.cs new file mode 100644 index 0000000..62c5091 --- /dev/null +++ b/TextoComEmbedding.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson; + +namespace ChatApi +{ + public class TextoComEmbedding + { + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public string Conteudo { get; set; } + public double[] Embedding { get; set; } + } +} \ No newline at end of file diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..0f2755a --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,19 @@ +{ + "DomvsDatabase": { + "ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "DomvsSites", + "SharepointCollectionName": "SharepointSite", + "ChatBotRHCollectionName": "ChatBotRHData", + "ClassifierCollectionName": "ClassifierData" + }, + "ChatRHSettings": { + "Url": "https://localhost:7070/", + "Create": "/CallRH" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..4c39c1b --- /dev/null +++ b/appsettings.json @@ -0,0 +1,22 @@ +{ + "DomvsDatabase": { + "ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "DomvsSites", + "SharepointCollectionName": "SharepointSite", + "ChatBotRHCollectionName": "ChatBotRHData", + "ClassifierCollectionName": "ClassifierData" + }, + "ChatRHSettings": { + "Url": "mongodb://localhost:27017", + "Create": "DomvsSites" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AppTenantId": "20190830-5fd4-4a72-b8fd-1c1cb35b25bc", + "AppClientID": "8f4248fc-ee30-4f54-8793-66edcca3fd20" +}