From 9ae0748ec6357abca9fb36c2ded15b884dcaf91a Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Mon, 9 Jun 2025 23:06:37 -0300 Subject: [PATCH] Add project files. --- .dockerignore | 30 +++ .editorconfig | 4 + .gitignore | 7 + AuthMiddleware.cs | 11 + ChatApi.http | 6 + ChatHistoryService.cs | 94 +++++++ ChatHistoryStore.cs | 7 + ChatRAG.csproj | 32 +++ ChatRAG.csproj.user | 11 + ChatRAG.sln | 25 ++ Controllers/ChatController.cs | 108 ++++++++ Controllers/LoginController.cs | 123 +++++++++ Controllers/LoginRequest.cs | 9 + Controllers/UserRequest.cs | 16 ++ DBLoadRequest.cs | 7 + Data/TextData.cs | 106 ++++++++ Data/UserDataRepository.cs | 45 ++++ Dockerfile | 25 ++ DomvsDatabaseSettings.cs | 17 ++ Infra/Result.cs | 56 ++++ Middlewares/ErrorHandlingMiddleware.cs | 47 ++++ Models/TextoComEmbedding.cs | 25 ++ Models/UserData.cs | 38 +++ Program.cs | 244 ++++++++++++++++++ Properties/launchSettings.json | 52 ++++ PushContainer.bat | 4 + Repositories/TextDataService.cs | 40 +++ Requests/ChatRequest.cs | 15 ++ Requests/FileData.cs | 9 + Services/Crypt/CryptUtil.cs | 66 +++++ Services/Emails/EmailValidate.cs | 24 ++ Services/ResponseService/IResponseService.cs | 9 + .../ResponseService/IResponseWithFiles.cs | 9 + .../ResponseService/ResponseCompanyService.cs | 132 ++++++++++ Services/TextFilter.cs | 33 +++ Settings/ChatRHSettings.cs | 8 + appsettings.Development.json | 20 ++ appsettings.json | 35 +++ 38 files changed, 1549 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 AuthMiddleware.cs create mode 100644 ChatApi.http create mode 100644 ChatHistoryService.cs create mode 100644 ChatHistoryStore.cs create mode 100644 ChatRAG.csproj create mode 100644 ChatRAG.csproj.user create mode 100644 ChatRAG.sln create mode 100644 Controllers/ChatController.cs create mode 100644 Controllers/LoginController.cs create mode 100644 Controllers/LoginRequest.cs create mode 100644 Controllers/UserRequest.cs create mode 100644 DBLoadRequest.cs create mode 100644 Data/TextData.cs create mode 100644 Data/UserDataRepository.cs create mode 100644 Dockerfile create mode 100644 DomvsDatabaseSettings.cs create mode 100644 Infra/Result.cs create mode 100644 Middlewares/ErrorHandlingMiddleware.cs create mode 100644 Models/TextoComEmbedding.cs create mode 100644 Models/UserData.cs create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json create mode 100644 PushContainer.bat create mode 100644 Repositories/TextDataService.cs create mode 100644 Requests/ChatRequest.cs create mode 100644 Requests/FileData.cs create mode 100644 Services/Crypt/CryptUtil.cs create mode 100644 Services/Emails/EmailValidate.cs create mode 100644 Services/ResponseService/IResponseService.cs create mode 100644 Services/ResponseService/IResponseWithFiles.cs create mode 100644 Services/ResponseService/ResponseCompanyService.cs create mode 100644 Services/TextFilter.cs create mode 100644 Settings/ChatRHSettings.cs create mode 100644 appsettings.Development.json create mode 100644 appsettings.json 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/.gitignore b/.gitignore new file mode 100644 index 0000000..4828efc --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +################################################################################ +# This .gitignore file was automatically created by Microsoft(R) Visual Studio. +################################################################################ + +/.vs +/bin/Debug/net8.0 +/obj 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.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/ChatHistoryService.cs b/ChatHistoryService.cs new file mode 100644 index 0000000..c72abc5 --- /dev/null +++ b/ChatHistoryService.cs @@ -0,0 +1,94 @@ +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(); + PromptLiliana(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 PromptLiliana(List msg) + { + msg.Add(new ChatMessageContent(AuthorRole.System, "Seu nome é LiliAna, um assistente virtual da Domvs It (Consultoria e RH).")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Você responde sempre em português do Brasil e fala sobre serviços prestados pela Domvs iT.")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Você consegue fazer solicitações no portal do RH da Domvs iT.")); + msg.Add(new ChatMessageContent(AuthorRole.System, "Você responde perguntas voltadas ao mercado de trabalho de tecnologia.")); + 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/ChatRAG.csproj b/ChatRAG.csproj new file mode 100644 index 0000000..dff7904 --- /dev/null +++ b/ChatRAG.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + 10e5023f-8f45-46d6-8637-bc2127842068 + Linux + . + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ChatRAG.csproj.user b/ChatRAG.csproj.user new file mode 100644 index 0000000..e5a2ec0 --- /dev/null +++ b/ChatRAG.csproj.user @@ -0,0 +1,11 @@ + + + + http + ApiControllerEmptyScaffolder + root/Common/Api + + + ProjectDebugger + + \ No newline at end of file diff --git a/ChatRAG.sln b/ChatRAG.sln new file mode 100644 index 0000000..10f934c --- /dev/null +++ b/ChatRAG.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}") = "ChatRAG", "ChatRAG.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/Controllers/ChatController.cs b/Controllers/ChatController.cs new file mode 100644 index 0000000..c89c4c5 --- /dev/null +++ b/Controllers/ChatController.cs @@ -0,0 +1,108 @@ +using Microsoft.AspNetCore.Mvc; +using ChatApi.Services; +using ChatApi.Services.ResponseService; +using Microsoft.AspNetCore.Authorization; +using ChatApi.Models; +using ChatApi.Data; +using ChatRAG.Services.ResponseService; +using ChatRAG.Services; + +#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]")] + //[Authorize] + public class ChatController : ControllerBase + { + private readonly ILogger _logger; + private readonly IResponseService _responseService; + private readonly TextFilter _textFilter; + private readonly UserDataRepository _userDataRepository; + private readonly TextData _textData; + private readonly IHttpClientFactory _httpClientFactory; + + public ChatController( + ILogger logger, + IResponseService responseService, + UserDataRepository userDataRepository, + TextData textData, + IHttpClientFactory httpClientFactory) + { + _logger = logger; + _responseService = responseService; + _userDataRepository = userDataRepository; + _textData = textData; + this._httpClientFactory = httpClientFactory; + } + + [HttpPost] + [Route("response")] + [Authorize] + public async Task GetResponse([FromForm] ChatRequest chatRequest) + { + var userData = await _userDataRepository.GeByTokentAsync(AppDomain.CurrentDomain.GetData("Token") as string); + var response = await _responseService.GetResponse(userData, chatRequest.SessionId, chatRequest.Message); + return response; + } + + [HttpPost] + [Route("savetext")] + public async Task SaveSingleText([FromBody] TextRequest request) + { + await _textData.SalvarNoMongoDB(request.Id, request.Title, request.Content); + } + + [HttpGet] + [Route("texts")] + public async Task> GetTexts() + { + var texts = await _textData.GetAll(); + return texts.Select(t => { + return new TextResponse + { + Id = t.Id, + Title = t.Titulo, + Content = t.Conteudo + }; + }); + } + + [HttpGet] + [Route("texts/id/{id}")] + public async Task GetText([FromRoute] string id) + { + var textItem = await _textData.GetById(id); + + return new TextResponse { + Id = textItem.Id, + Title = textItem.Titulo, + Content = textItem.Conteudo + }; + } + + [HttpGet] + [Route("health")] + [AllowAnonymous] + public IActionResult Health() + { + return Ok("It´s online."); + } + + } + + public class TextRequest + { + public string? Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + } + public class TextResponse + { + public string? Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + } +} +#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/LoginController.cs b/Controllers/LoginController.cs new file mode 100644 index 0000000..6922a17 --- /dev/null +++ b/Controllers/LoginController.cs @@ -0,0 +1,123 @@ +using ChatApi.Models; +using ChatApi.Services.Crypt; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Cors; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; + +namespace ChatApi.Controllers +{ + [Route("[controller]")] + [ApiController] + [EnableCors("AllowSpecificOrigin")] + public class LoginController : ControllerBase + { + private readonly IConfigurationManager _configuration; + private readonly UserDataRepository _userDataRepository; + private readonly CryptUtil _cryptUtil; + + public LoginController(IConfigurationManager configuration, UserDataRepository userDataRepository, CryptUtil cryptUtil) + { + _configuration = configuration; + _userDataRepository = userDataRepository; + _cryptUtil = cryptUtil; + } + + [AllowAnonymous] + [HttpPost] + [Route("token")] + public async Task Post([FromBody] LoginRequest loginRequest) + { + if (ModelState.IsValid) + { + try + { + var userDataFrom = await _userDataRepository.GetAsync(loginRequest.ClientName, loginRequest.ClientId, loginRequest.ClientSecret); + if (userDataFrom==null) + { + return Unauthorized(); + } + + var token = ""; + if (userDataFrom.LastToken == null && (userDataFrom.DateTimeToken != null && userDataFrom.DateTimeToken.Value.AddHours(24) > DateTime.Now)) + { + token = userDataFrom.LastToken; + } + else + { + var claims = new[] + { + new Claim("Sub", userDataFrom.CompanyTenant), + new Claim("NameId", userDataFrom.Name), + new Claim(ClaimTypes.NameIdentifier, loginRequest.ClientId), + new Claim("DhCriado", DateTime.Now.ToString(new CultureInfo("pt-BR"))), + new Claim("TenantId", userDataFrom.CompanyTenant), + new Claim(ClaimTypes.Role, "TeamsUser") + }; + + var expires = DateTime.UtcNow.AddMinutes(30); + var tokenGen = new JwtSecurityToken + ( + issuer: _configuration["Issuer"], + audience: _configuration["Audience"], + claims: claims, + expires: expires, + notBefore: DateTime.UtcNow, + signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_configuration["SigningKey"])), + SecurityAlgorithms.HmacSha256) + ); + + token = new JwtSecurityTokenHandler().WriteToken(tokenGen); + } + userDataFrom.LastToken = token; + userDataFrom.DateTimeToken = DateTime.Now; + await _userDataRepository.UpdateAsync(userDataFrom.Id, userDataFrom); + + return Ok(new { token = token }); + } + catch (Exception ex) + { + return StatusCode(500, ex.Message); + } + } + + return BadRequest(); + } + + [AllowAnonymous] + [HttpPost] + [Route("newclient")] + public async Task NewClient([FromBody] UserRequest userDataFrom) + { + if (ModelState.IsValid) + { + try + { + var userData = await _userDataRepository.GetAsync(userDataFrom.Name, userDataFrom.LocalId); + if (userData == null) + { + var secret = _cryptUtil.Encrypt(JsonSerializer.Serialize(userDataFrom)); + userData = UserData.Create(userDataFrom, secret); + await _userDataRepository.CreateAsync(userData); + } + + return Created("newclient", userData); + } + catch (Exception ex) + { + return StatusCode(500, ex.Message); + } + } + + return BadRequest(); + } + } +} diff --git a/Controllers/LoginRequest.cs b/Controllers/LoginRequest.cs new file mode 100644 index 0000000..d32158e --- /dev/null +++ b/Controllers/LoginRequest.cs @@ -0,0 +1,9 @@ +namespace ChatApi.Controllers +{ + public class LoginRequest + { + public string ClientId { get; set; } + public string ClientName { get; set; } + public string ClientSecret { get; set; } + } +} diff --git a/Controllers/UserRequest.cs b/Controllers/UserRequest.cs new file mode 100644 index 0000000..dca2b6d --- /dev/null +++ b/Controllers/UserRequest.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson; +using System.Text.Json.Serialization; + +namespace ChatApi.Controllers +{ + public class UserRequest + { + public string Name { get; set; } + public string CompanyTenant { get; set; } + public string LocalId { get; set; } + + [JsonIgnore] + public string? Secret { get; set; } + } +} \ No newline at end of file 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/Data/TextData.cs b/Data/TextData.cs new file mode 100644 index 0000000..1f18de1 --- /dev/null +++ b/Data/TextData.cs @@ -0,0 +1,106 @@ +using ChatRAG.Models; +using ChatRAG.Repositories; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Embeddings; +using System.Text; + +#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.Data +{ + public class TextData + { + private readonly ITextEmbeddingGenerationService _textEmbeddingGenerationService; + private readonly TextDataService _textDataService; + + public TextData(ITextEmbeddingGenerationService textEmbeddingGenerationService, TextDataService textDataService) + { + _textEmbeddingGenerationService = textEmbeddingGenerationService; + _textDataService = textDataService; + } + + public 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); + } + } + + foreach(var item in textoArray) + { + await SalvarNoMongoDB(title, item); + } + } + + public async Task SalvarNoMongoDB(string titulo, string texto) + { + await SalvarNoMongoDB(null, titulo, texto); + } + + public async Task SalvarNoMongoDB(string? id, string titulo, string texto) + { + // Gerar embedding para o texto + var embedding = await _textEmbeddingGenerationService.GenerateEmbeddingAsync(texto); + + // Converter embedding para um formato serializável (como um array de floats) + var embeddingArray = embedding.ToArray().Select(e => (double)e).ToArray(); + + var exists = id!=null ? await this.GetById(id) : null; + + if (exists == null) + { + var documento = new TextoComEmbedding + { + Titulo = titulo, + Conteudo = texto, + Embedding = embeddingArray + }; + + await _textDataService.CreateAsync(documento); + } + else + { + var documento = new TextoComEmbedding + { + Id = id, + Titulo = titulo, + Conteudo = texto, + Embedding = embeddingArray + }; + + await _textDataService.UpdateAsync(id, documento); + } + } + + public async Task> GetAll() + { + return await _textDataService.GetAsync(); + } + public async Task GetById(string id) + { + return await _textDataService.GetAsync(id); + } + } +} +#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/Data/UserDataRepository.cs b/Data/UserDataRepository.cs new file mode 100644 index 0000000..f48011a --- /dev/null +++ b/Data/UserDataRepository.cs @@ -0,0 +1,45 @@ +using ChatApi.Models; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace ChatApi +{ + public class UserDataRepository + { + private readonly IMongoCollection _userCollection; + + public UserDataRepository( + IOptions bookStoreDatabaseSettings) + { + var mongoClient = new MongoClient( + bookStoreDatabaseSettings.Value.ConnectionString); + + var mongoDatabase = mongoClient.GetDatabase( + bookStoreDatabaseSettings.Value.DatabaseName); + + _userCollection = mongoDatabase.GetCollection( + bookStoreDatabaseSettings.Value.UserDataName); + } + + public async Task> GetAsync() => + await _userCollection.Find(_ => true).ToListAsync(); + + public async Task GeByTokentAsync(string token) => + await _userCollection.Find(x => x.LastToken == token).FirstOrDefaultAsync(); + public async Task GetAsync(string id) => + await _userCollection.Find(x => x.Id == id).FirstOrDefaultAsync(); + public async Task GetAsync(string name, string localId) => + await _userCollection.Find(x => x.Name == name && x.LocalId == localId).FirstOrDefaultAsync(); + public async Task GetAsync(string name, string localId, string secret) => + await _userCollection.Find(x => x.Name == name && x.LocalId == localId && x.Secret == secret).FirstOrDefaultAsync(); + + public async Task CreateAsync(UserData newBook) => + await _userCollection.InsertOneAsync(newBook); + + public async Task UpdateAsync(string id, UserData updatedBook) => + await _userCollection.ReplaceOneAsync(x => x.Id == id, updatedBook); + + public async Task RemoveAsync(string id) => + await _userCollection.DeleteOneAsync(x => x.Id == id); + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a95b7bc --- /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"] diff --git a/DomvsDatabaseSettings.cs b/DomvsDatabaseSettings.cs new file mode 100644 index 0000000..5c310e1 --- /dev/null +++ b/DomvsDatabaseSettings.cs @@ -0,0 +1,17 @@ +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 UserDataName { 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/Middlewares/ErrorHandlingMiddleware.cs b/Middlewares/ErrorHandlingMiddleware.cs new file mode 100644 index 0000000..1063a1f --- /dev/null +++ b/Middlewares/ErrorHandlingMiddleware.cs @@ -0,0 +1,47 @@ +using System.Net; +using System.Text.Json; + +namespace ChatApi.Middlewares +{ + public class ErrorHandlingMiddleware + { + readonly RequestDelegate _next; + static ILogger _logger; + + public ErrorHandlingMiddleware(RequestDelegate next, + ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task Invoke(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + await HandleExceptionAsync(context, ex); + } + } + + static async Task HandleExceptionAsync( + HttpContext context, + Exception exception) + { + string error = "An internal server error has occured."; + + _logger.LogError($"{exception.Source} - {exception.Message} - {exception.StackTrace} - {exception.TargetSite.Name}"); + + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + context.Response.ContentType = "application/json"; + + await context.Response.WriteAsync(JsonSerializer.Serialize(new + { + error + })); + } + } +} diff --git a/Models/TextoComEmbedding.cs b/Models/TextoComEmbedding.cs new file mode 100644 index 0000000..fc0c274 --- /dev/null +++ b/Models/TextoComEmbedding.cs @@ -0,0 +1,25 @@ +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson; + +namespace ChatRAG.Models +{ + public class TextoComEmbedding + { + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.ObjectId)] + public string Id { get; set; } + + public string Titulo { get; set; } + //public string Título { get; set; } + public string Conteudo { get; set; } + public double[] Embedding { get; set; } + + public string ProjetoNome { get; set; } + public string ProjetoId { get; set; } // Para referência se tiver tabela de projetos + public string TipoDocumento { get; set; } // "requisitos", "arquitetura", "casos_teste", etc. + public string Categoria { get; set; } // "login", "relatorios", "api", etc. + public DateTime DataCriacao { get; set; } + public string[] Tags { get; set; } // Para busca adicional + } +} \ No newline at end of file diff --git a/Models/UserData.cs b/Models/UserData.cs new file mode 100644 index 0000000..571e444 --- /dev/null +++ b/Models/UserData.cs @@ -0,0 +1,38 @@ +using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Bson; +using System.Security.Claims; +using ChatApi.Controllers; + +namespace ChatApi.Models +{ + public class UserData + { + public UserData() + { + } + + [BsonId] + [BsonElement("_id")] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + public string LocalId { get; set; } + public string Name { get; set; } + public string Secret { get; set; } + public string CompanyTenant { get; set; } + public string? Email { get; set; } + public string? LastToken { get; set; } + public DateTime? DateTimeToken { get; set; } + + public static UserData Create(UserRequest userRequest, string secret) + { + return new UserData + { + Id = Guid.NewGuid().ToString("N"), + Name = userRequest.Name, + CompanyTenant = userRequest.CompanyTenant, + LocalId = userRequest.LocalId, + Secret = secret + }; + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..d52b5c7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,244 @@ +using ChatApi; +using ChatApi.Data; +using ChatApi.Middlewares; +using ChatApi.Services.Crypt; +using ChatApi.Settings; +using ChatRAG.Repositories; +using ChatRAG.Services; +using ChatRAG.Services.ResponseService; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; +using Microsoft.SemanticKernel; +using System.Text; +using static OllamaSharp.OllamaApiClient; +using static System.Net.Mime.MediaTypeNames; + +#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. + +// Adicionar serviço CORS +builder.Services.AddCors(options => +{ + options.AddPolicy("AllowSpecificOrigin", + builder => + { + builder + .WithOrigins("http://localhost:5094") // Sua origem específica + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); +}); + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo { Title = "apichat", Version = "v1" }); + + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer'[space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"", + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + new string[] {} + } + }); +}); + +builder.Services.Configure( +builder.Configuration.GetSection("DomvsDatabase")); + +builder.Services.Configure( +builder.Configuration.GetSection("ChatRHSettings")); + +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); + +builder.Services.AddScoped(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +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"); + +//Olllama notebook +//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=="); + +//Ollama local server (scorpion) +//builder.Services.AddOllamaChatCompletion("llama3.1:latest", new Uri("http://192.168.0.150:11434")); + +builder.Services.AddOllamaTextEmbeddingGeneration("all-minilm", new Uri("http://192.168.0.150:11434")); +//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(jwtBearerOptions => +{ + jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters() + { + ValidateActor = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = builder.Configuration["Issuer"], + ValidAudience = builder.Configuration["Audience"], + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["SigningKey"])) + }; + jwtBearerOptions.Events = new JwtBearerEvents() + { + OnTokenValidated = async context => + { + var token = context.SecurityToken as JsonWebToken; + context.HttpContext.Items["Token"] = token.EncodedToken; + AppDomain.CurrentDomain.SetData("Token", token.EncodedToken); + } + }; +}) +; + +//builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) +// .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")); + +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; +// } +// }; +// }); + +builder.Services.AddSingleton(builder.Configuration); + +builder.Services.Configure(options => +{ + options.MaxRequestBodySize = int.MaxValue; +}); + +builder.Services.Configure(options => +{ + options.Limits.MaxRequestBodySize = int.MaxValue; +}); + +builder.Services.Configure(options => +{ + options.ValueLengthLimit = int.MaxValue; + options.MultipartBodyLengthLimit = int.MaxValue; + options.MultipartHeadersLengthLimit = int.MaxValue; +}); + +var app = builder.Build(); + + + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +//app.UseHttpsRedirection(); + +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.UseMiddleware(); + +app.UseCors("AllowSpecificOrigin"); + +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/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/PushContainer.bat b/PushContainer.bat new file mode 100644 index 0000000..7ee6faf --- /dev/null +++ b/PushContainer.bat @@ -0,0 +1,4 @@ +cd C:\vscode\ChatApi +docker build -t chat-api . +docker tag chat-api:latest registry.redecarneir.us/chat-api:latest +docker push registry.redecarneir.us/chat-api:latest diff --git a/Repositories/TextDataService.cs b/Repositories/TextDataService.cs new file mode 100644 index 0000000..f81a3be --- /dev/null +++ b/Repositories/TextDataService.cs @@ -0,0 +1,40 @@ +using ChatApi; +using ChatRAG.Models; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace ChatRAG.Repositories +{ + public class TextDataService + { + private readonly IMongoCollection _textsCollection; + + public TextDataService( + IOptions bookStoreDatabaseSettings) + { + var mongoClient = new MongoClient( + bookStoreDatabaseSettings.Value.ConnectionString); + + var mongoDatabase = mongoClient.GetDatabase( + bookStoreDatabaseSettings.Value.DatabaseName); + + _textsCollection = mongoDatabase.GetCollection( + bookStoreDatabaseSettings.Value.SharepointCollectionName); + } + + public async Task> GetAsync() => + await _textsCollection.Find(_ => true).ToListAsync(); + + public async Task GetAsync(string id) => + await _textsCollection.Find(x => x.Id == id).FirstOrDefaultAsync(); + + public async Task CreateAsync(TextoComEmbedding newBook) => + await _textsCollection.InsertOneAsync(newBook); + + public async Task UpdateAsync(string id, TextoComEmbedding updatedBook) => + await _textsCollection.ReplaceOneAsync(x => x.Id == id, updatedBook); + + public async Task RemoveAsync(string id) => + await _textsCollection.DeleteOneAsync(x => x.Id == id); + } +} diff --git a/Requests/ChatRequest.cs b/Requests/ChatRequest.cs new file mode 100644 index 0000000..9bb4650 --- /dev/null +++ b/Requests/ChatRequest.cs @@ -0,0 +1,15 @@ +using System; + +public class ChatRequest +{ + public string SessionId { get; set; } = string.Empty; + public string Message { get; set; } = string.Empty; +} + + +public class VideoSummaryRequest +{ + public string SessionId { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; + public string Language { get; set; } = string.Empty; +} diff --git a/Requests/FileData.cs b/Requests/FileData.cs new file mode 100644 index 0000000..08503fc --- /dev/null +++ b/Requests/FileData.cs @@ -0,0 +1,9 @@ +namespace ChatApi.Requests +{ + public class FileData + { + public string FileName { get; set; } = string.Empty; + public string ContentType { get; set; } = string.Empty; + public byte[] Content { get; set; } = Array.Empty(); + } +} diff --git a/Services/Crypt/CryptUtil.cs b/Services/Crypt/CryptUtil.cs new file mode 100644 index 0000000..31cc133 --- /dev/null +++ b/Services/Crypt/CryptUtil.cs @@ -0,0 +1,66 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace ChatApi.Services.Crypt +{ + public class CryptUtil + { + private readonly IConfigurationManager _configuration; + + public CryptUtil(IConfigurationManager configuration) + { + _configuration = configuration; + } + public string Encrypt(string text) + { + string key = _configuration.GetSection("DataKey").Value; + using (Aes aes = Aes.Create()) + { + aes.Key = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32)); + aes.GenerateIV(); // Cria um vetor de inicialização aleatório + byte[] iv = aes.IV; + + using (var encryptor = aes.CreateEncryptor(aes.Key, iv)) + using (var ms = new MemoryStream()) + { + // Primeiro, escreva o IV para o fluxo (será necessário para descriptografia) + ms.Write(iv, 0, iv.Length); + + using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + using (var writer = new StreamWriter(cs)) + { + writer.Write(text); + } + + return Convert.ToBase64String(ms.ToArray()); + } + } + } + + public string Descrypt(string text) + { + string key = _configuration.GetSection("DataKey").Value; + byte[] bytes = Convert.FromBase64String(text); + + using (Aes aes = Aes.Create()) + { + aes.Key = Encoding.UTF8.GetBytes(key.PadRight(32).Substring(0, 32)); + + // Extrair o IV do início dos dados criptografados + byte[] iv = new byte[aes.BlockSize / 8]; + Array.Copy(bytes, 0, iv, 0, iv.Length); + aes.IV = iv; + + using (var decryptor = aes.CreateDecryptor(aes.Key, aes.IV)) + using (var ms = new MemoryStream(bytes, iv.Length, bytes.Length - iv.Length)) + using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) + using (var reader = new StreamReader(cs)) + { + return reader.ReadToEnd(); + } + } + } + } +} diff --git a/Services/Emails/EmailValidate.cs b/Services/Emails/EmailValidate.cs new file mode 100644 index 0000000..39cfc6c --- /dev/null +++ b/Services/Emails/EmailValidate.cs @@ -0,0 +1,24 @@ +namespace ChatApi.Services.Emails +{ + public class EmailValidate + { + public static bool IsValidEmail(string email) + { + var trimmedEmail = email.Trim(); + + if (trimmedEmail.EndsWith(".")) + { + return false; // suggested by @TK-421 + } + try + { + var addr = new System.Net.Mail.MailAddress(email); + return addr.Address == trimmedEmail; + } + catch + { + return false; + } + } + } +} diff --git a/Services/ResponseService/IResponseService.cs b/Services/ResponseService/IResponseService.cs new file mode 100644 index 0000000..9fe2877 --- /dev/null +++ b/Services/ResponseService/IResponseService.cs @@ -0,0 +1,9 @@ +using ChatApi.Models; + +namespace ChatRAG.Services.ResponseService +{ + public interface IResponseService + { + Task GetResponse(UserData userData, string sessionId, string question); + } +} diff --git a/Services/ResponseService/IResponseWithFiles.cs b/Services/ResponseService/IResponseWithFiles.cs new file mode 100644 index 0000000..268eac3 --- /dev/null +++ b/Services/ResponseService/IResponseWithFiles.cs @@ -0,0 +1,9 @@ +using ChatApi.Models; + +namespace ChatApi.Services.ResponseService +{ + public interface IResponseWithFiles + { + Task GetResponse(HttpContext context, UserData userData, string sessionId, string question, List? files = null, bool needsRestart = false); + } +} diff --git a/Services/ResponseService/ResponseCompanyService.cs b/Services/ResponseService/ResponseCompanyService.cs new file mode 100644 index 0000000..5796d2d --- /dev/null +++ b/Services/ResponseService/ResponseCompanyService.cs @@ -0,0 +1,132 @@ + +using ChatApi; +using ChatApi.Models; +using ChatRAG.Models; +using ChatRAG.Repositories; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Embeddings; + +#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 ChatRAG.Services.ResponseService +{ + public class ResponseRAGService : IResponseService + { + private readonly ChatHistoryService _chatHistoryService; + private readonly Kernel _kernel; + private readonly TextFilter _textFilter; + private readonly TextDataService _textDataService; + private readonly IChatCompletionService _chatCompletionService; + + public ResponseRAGService( + ChatHistoryService chatHistoryService, + Kernel kernel, + TextFilter textFilter, + TextDataService textDataService, + IChatCompletionService chatCompletionService) + { + this._chatHistoryService = chatHistoryService; + this._kernel = kernel; + this._textFilter = textFilter; + this._textDataService = textDataService; + this._chatCompletionService = chatCompletionService; + } + public async Task GetResponse(UserData userData, string sessionId, string question) + { + var stopWatch = new System.Diagnostics.Stopwatch(); + stopWatch.Start(); + + //var resposta = await BuscarTextoRelacionado(question); + var resposta = await BuscarTopTextosRelacionados(question); + + question = "Para responder à solicitação/pergunta: \"" + question + "\" por favor, considere 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 _textDataService.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; + } + } + + return melhorTexto != null ? melhorTexto.Conteudo : "Não encontrei uma resposta adequada."; + } + + async Task BuscarTopTextosRelacionados(string pergunta, int size = 3) + { + var embeddingService = _kernel.GetRequiredService(); + var embeddingPergunta = await embeddingService.GenerateEmbeddingAsync(_textFilter.ToLowerAndWithoutAccents(pergunta)); + var embeddingArrayPergunta = embeddingPergunta.ToArray().Select(e => (double)e).ToArray(); + + var textos = await _textDataService.GetAsync(); + + var melhoresTextos = textos + .Select(texto => new + { + Conteudo = texto.Conteudo, + Similaridade = CalcularSimilaridadeCoseno(embeddingArrayPergunta, texto.Embedding) + }) + .Where(x => x.Similaridade > 0.3) + .OrderByDescending(x => x.Similaridade) + .Take(3) + .ToList(); + + if (!melhoresTextos.Any()) + return "Não encontrei respostas adequadas para a pergunta fornecida."; + + var cabecalho = $"Contexto encontrado para: '{pergunta}' ({melhoresTextos.Count} resultado(s)):\n\n"; + + var resultadosFormatados = melhoresTextos + .Select((item, index) => + $"=== CONTEXTO {index + 1} ===\n" + + $"Relevância: {item.Similaridade:P1}\n" + + $"Conteúdo:\n{item.Conteudo}") + .ToList(); + + return cabecalho + string.Join("\n\n", resultadosFormatados); + } + + 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/TextFilter.cs b/Services/TextFilter.cs new file mode 100644 index 0000000..1c2aae4 --- /dev/null +++ b/Services/TextFilter.cs @@ -0,0 +1,33 @@ +using System.Globalization; +using System.Text; + +namespace ChatRAG.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/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/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..9e4b091 --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "DomvsDatabase": { + //"ConnectionString": "mongodb://192.168.0.82:30017/?directConnection=true", + "ConnectionString": "mongodb://localhost:27017/?directConnection=true", + "DatabaseName": "DomvsSites", + "SharepointCollectionName": "SharepointSite", + "ChatBotRHCollectionName": "ChatBotRHData", + "ClassifierCollectionName": "ClassifierData" + }, + "ChatRHSettings": { + "Url": "http://localhost:8070/", + "Create": "/CallRH" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/appsettings.json b/appsettings.json new file mode 100644 index 0000000..a300e43 --- /dev/null +++ b/appsettings.json @@ -0,0 +1,35 @@ +{ + "DomvsDatabase": { + "ConnectionString": "mongodb://192.168.0.82:30017/?directConnection=true", + "DatabaseName": "DomvsSites", + "SharepointCollectionName": "SharepointSite", + "ChatBotRHCollectionName": "ChatBotRHData", + "ClassifierCollectionName": "ClassifierData", + "UserDataName": "UserData" + }, + "ChatRHSettings": { + "Url": "http://apirhcall.lhost.dynu.net", + "Create": "/CallRH" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.DataProtection": "None" + } + }, + "AllowedHosts": "*", + "AppTenantId": "20190830-5fd4-4a72-b8fd-1c1cb35b25bc", + "AppClientID": "8f4248fc-ee30-4f54-8793-66edcca3fd20", + + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "domvsitbr.onmicrosoft.com", + "TenantId": "20190830-5fd4-4a72-b8fd-1c1cb35b25bc", + "ClientId": "8f4248fc-ee30-4f54-8793-66edcca3fd20" + }, + "Issuer": "domvsit.com.br", + "Audience": "domvsit.com.br", + "SigningKey": "D57Ls16KxMPdF4P7qTQtV29slWjJqIJZ", + "DataKey": "NOxGacYtZRJTYCPdSQM75HVSNp3qfH05mPalaE/pL4A6FwxWKQiBhLxhu++LrKsI" +}