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" +}