Changes
Build and Push Docker Images / build (push) Failing after 28s

This commit is contained in:
2026-05-06 17:45:05 +03:00
parent 64b0219038
commit b154fe51c3
15 changed files with 50 additions and 110 deletions
+3 -2
View File
@@ -1,5 +1,6 @@
using Refit; using Refit;
using Models.Requests; using Models.Requests;
using CvMatcher.Models.Responses;
namespace Api.Clients.Api.Contracts; namespace Api.Clients.Api.Contracts;
@@ -7,8 +8,8 @@ public interface ICvMatcherApi
{ {
[Multipart] [Multipart]
[Post("/api/cv/upload")] [Post("/api/cv/upload")]
Task<HttpResponseMessage> Upload([AliasAs("cv")] StreamPart cv, [AliasAs("gdprConsent")] bool gdprConsent); Task<CvUploadResponse> Upload([AliasAs("cv")] StreamPart cv, [AliasAs("gdprConsent")] bool gdprConsent);
[Post("/api/cv/match-job")] [Post("/api/cv/match-job")]
Task<HttpResponseMessage> MatchJob([Body] JobMatchRequest request); Task<JobMatchResponse> MatchJob([Body] JobMatchRequest request);
} }
+4 -16
View File
@@ -70,8 +70,8 @@ public sealed class CvMatcherController : ControllerBase
var stream = cv.OpenReadStream(); var stream = cv.OpenReadStream();
var part = new Refit.StreamPart(stream, cv.FileName, "application/pdf"); var part = new Refit.StreamPart(stream, cv.FileName, "application/pdf");
using var response = await _cvApi.Upload(part, gdprConsent); var res = await _cvApi.Upload(part, gdprConsent);
return await ProxyResponseAsync(response, ct); return Ok(res);
} }
catch (OperationCanceledException) when (ct.IsCancellationRequested) catch (OperationCanceledException) when (ct.IsCancellationRequested)
{ {
@@ -114,9 +114,8 @@ public sealed class CvMatcherController : ControllerBase
request.CvDocumentId, request.CvDocumentId,
!string.IsNullOrWhiteSpace(request.JobUrl), !string.IsNullOrWhiteSpace(request.JobUrl),
!string.IsNullOrWhiteSpace(request.JobDescription)); !string.IsNullOrWhiteSpace(request.JobDescription));
var res = await _cvApi.MatchJob(request);
using var response = await _cvApi.MatchJob(request); return Ok(res);
return await ProxyResponseAsync(response, ct);
} }
catch (OperationCanceledException) when (ct.IsCancellationRequested) catch (OperationCanceledException) when (ct.IsCancellationRequested)
{ {
@@ -129,15 +128,4 @@ public sealed class CvMatcherController : ControllerBase
return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API request failed." }); return StatusCode(StatusCodes.Status502BadGateway, new { error = "CV matcher API request failed." });
} }
} }
private static async Task<ContentResult> ProxyResponseAsync(HttpResponseMessage response, CancellationToken ct)
{
var body = await response.Content.ReadAsStringAsync(ct);
return new ContentResult
{
StatusCode = (int)response.StatusCode,
Content = body,
ContentType = response.Content.Headers.ContentType?.ToString() ?? "application/json"
};
}
} }
+1
View File
@@ -7,5 +7,6 @@ namespace Api.Services.Contracts
Task SendContactAsync(ContactRequest req, CancellationToken ct); Task SendContactAsync(ContactRequest req, CancellationToken ct);
Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct); Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct);
Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct); Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct);
Task SendMatchAsync(string? explicitTo, string subject, string body, CancellationToken ct);
} }
} }
+5
View File
@@ -166,5 +166,10 @@ namespace Api.Services
await client.SendAsync(message, ct); await client.SendAsync(message, ct);
await client.DisconnectAsync(true, ct); await client.DisconnectAsync(true, ct);
} }
public Task SendMatchAsync(string? explicitTo, string subject, string body, CancellationToken ct)
{
throw new NotImplementedException();
}
} }
} }
+1
View File
@@ -36,6 +36,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\api-models\api-models.csproj" /> <ProjectReference Include="..\api-models\api-models.csproj" />
<ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -7,6 +7,5 @@
public string? JobDescription { get; set; } public string? JobDescription { get; set; }
public bool GdprConsent { get; set; } public bool GdprConsent { get; set; }
public string? Email { get; set; } public string? Email { get; set; }
public string? CaptchaToken { get; set; }
} }
} }
+6 -5
View File
@@ -1,6 +1,7 @@
using CvMatcher.Models.Requests; using CvMatcher.Models.Requests;
using Api.Services.Contracts; using Api.Services.Contracts;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using CvMatcher.Models.Responses;
namespace Api.Controllers; namespace Api.Controllers;
@@ -19,7 +20,7 @@ public sealed class CvController : ControllerBase
[HttpPost("upload")] [HttpPost("upload")]
[RequestSizeLimit(10 * 1024 * 1024)] [RequestSizeLimit(10 * 1024 * 1024)]
public async Task<IActionResult> Upload([FromForm(Name = "cv")] IFormFile? cv, [FromForm] bool gdprConsent, CancellationToken ct) public async Task<ActionResult<CvUploadResponse>> Upload([FromForm(Name = "cv")] IFormFile? cv, [FromForm] bool gdprConsent, CancellationToken ct)
{ {
try try
{ {
@@ -37,14 +38,14 @@ public sealed class CvController : ControllerBase
} }
[HttpPost("find-jobs")] [HttpPost("find-jobs")]
public async Task<IActionResult> FindJobs([FromBody] FindJobsRequest request, CancellationToken ct) public async Task<ActionResult<FindJobsResponse>> FindJobs([FromBody] FindJobsRequest request, CancellationToken ct)
{ {
try try
{ {
_logger.LogInformation("Find jobs request received. CvDocumentId={CvDocumentId}, TopK={TopK}", request.CvDocumentId, request.TopK); _logger.LogInformation("Find jobs request received. CvDocumentId={CvDocumentId}, TopK={TopK}", request.CvDocumentId, request.TopK);
var result = await _service.FindJobsAsync(request, ct); var result = await _service.FindJobsAsync(request, ct);
_logger.LogInformation("Find jobs completed. CvDocumentId={CvDocumentId}, ResultCount={ResultCount}", request.CvDocumentId, result.Jobs.Count); _logger.LogInformation("Find jobs completed. CvDocumentId={CvDocumentId}, ResultCount={ResultCount}", request.CvDocumentId, result.Jobs.Count);
return Ok(result); return result;
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
@@ -54,7 +55,7 @@ public sealed class CvController : ControllerBase
} }
[HttpPost("match-job")] [HttpPost("match-job")]
public async Task<IActionResult> MatchJob([FromBody] MatchJobRequest request, CancellationToken ct) public async Task<ActionResult<JobMatchResponse>> MatchJob([FromBody] MatchJobRequest request, CancellationToken ct)
{ {
try try
{ {
@@ -62,7 +63,7 @@ public sealed class CvController : ControllerBase
request.CvDocumentId, !string.IsNullOrWhiteSpace(request.JobUrl), !string.IsNullOrWhiteSpace(request.JobDescription), !string.IsNullOrWhiteSpace(request.Email)); request.CvDocumentId, !string.IsNullOrWhiteSpace(request.JobUrl), !string.IsNullOrWhiteSpace(request.JobDescription), !string.IsNullOrWhiteSpace(request.Email));
var result = await _service.MatchJobAsync(request, ct); var result = await _service.MatchJobAsync(request, ct);
_logger.LogInformation("Match job completed. CvDocumentId={CvDocumentId}, Score={Score}, Cached={Cached}", request.CvDocumentId, result.Score, result.Cached); _logger.LogInformation("Match job completed. CvDocumentId={CvDocumentId}, Score={Score}, Cached={Cached}", request.CvDocumentId, result.Score, result.Cached);
return Ok(result); return result;
} }
catch (InvalidOperationException ex) catch (InvalidOperationException ex)
{ {
-1
View File
@@ -97,7 +97,6 @@ try
?? throw new InvalidOperationException("Connection string 'CvMatcherDb' is missing."))); ?? throw new InvalidOperationException("Connection string 'CvMatcherDb' is missing.")));
builder.Services.AddScoped<IMatcherRepository, EfMatcherRepository>(); builder.Services.AddScoped<IMatcherRepository, EfMatcherRepository>();
builder.Services.AddScoped<ICvMatcherService, CvMatcherService>(); builder.Services.AddScoped<ICvMatcherService, CvMatcherService>();
builder.Services.AddSingleton<IEmailService, EmailService>();
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
@@ -1,6 +0,0 @@
namespace Api.Services.Contracts;
public interface IEmailService
{
Task SendMatchAsync(string? explicitTo, string subject, string body, CancellationToken ct);
}
+20 -23
View File
@@ -16,7 +16,6 @@ public sealed class CvMatcherService : ICvMatcherService
private readonly IJobTextExtractor _jobTextExtractor; private readonly IJobTextExtractor _jobTextExtractor;
private readonly IMatcherAiClient _ai; private readonly IMatcherAiClient _ai;
private readonly IMatcherRepository _repository; private readonly IMatcherRepository _repository;
private readonly IEmailService _email;
private readonly MatcherSettings _settings; private readonly MatcherSettings _settings;
public CvMatcherService( public CvMatcherService(
@@ -24,14 +23,12 @@ public sealed class CvMatcherService : ICvMatcherService
IJobTextExtractor jobTextExtractor, IJobTextExtractor jobTextExtractor,
IMatcherAiClient ai, IMatcherAiClient ai,
IMatcherRepository repository, IMatcherRepository repository,
IEmailService email,
IOptions<MatcherSettings> options) IOptions<MatcherSettings> options)
{ {
_rag = rag; _rag = rag;
_jobTextExtractor = jobTextExtractor; _jobTextExtractor = jobTextExtractor;
_ai = ai; _ai = ai;
_repository = repository; _repository = repository;
_email = email;
_settings = options.Value; _settings = options.Value;
} }
@@ -138,11 +135,11 @@ public sealed class CvMatcherService : ICvMatcherService
result.Cached = false; result.Cached = false;
await _repository.SaveMatchAsync(cv.Id, job.Id, result, ct); await _repository.SaveMatchAsync(cv.Id, job.Id, result, ct);
await _email.SendMatchAsync( //await _email.SendMatchAsync(
email, // email,
$"MyAi.ro CV Match: {result.Score}% - {job.Title}", // $"MyAi.ro CV Match: {result.Score}% - {job.Title}",
BuildEmailBody(cv, job, result), // BuildEmailBody(cv, job, result),
ct); // ct);
return result; return result;
} }
@@ -181,24 +178,24 @@ public sealed class CvMatcherService : ICvMatcherService
private static string Limit(string value, int max) => value.Length <= max ? value : value[..max]; private static string Limit(string value, int max) => value.Length <= max ? value : value[..max];
private static string BuildEmailBody(RagDocumentDetails cv, RagDocumentDetails job, JobMatchResponse result) => $""" //private static string BuildEmailBody(RagDocumentDetails cv, RagDocumentDetails job, JobMatchResponse result) => $"""
CV Matcher result // CV Matcher result
CV: {cv.Title} // CV: {cv.Title}
Job: {job.Title} // Job: {job.Title}
Job URL: {job.SourceUrl ?? "N/A"} // Job URL: {job.SourceUrl ?? "N/A"}
Score: {result.Score}% // Score: {result.Score}%
Summary: // Summary:
{result.Summary} // {result.Summary}
Strengths: // Strengths:
- {string.Join("\n- ", result.Strengths)} // - {string.Join("\n- ", result.Strengths)}
Gaps: // Gaps:
- {string.Join("\n- ", result.Gaps)} // - {string.Join("\n- ", result.Gaps)}
Recommendations: // Recommendations:
- {string.Join("\n- ", result.Recommendations)} // - {string.Join("\n- ", result.Recommendations)}
"""; // """;
} }
-46
View File
@@ -1,46 +0,0 @@
using CvMatcher.Models.Settings;
using Api.Services.Contracts;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Options;
using MimeKit;
namespace Api.Services;
public sealed class EmailService : IEmailService
{
private readonly SmtpSettings _settings;
private readonly ILogger<EmailService> _logger;
public EmailService(IOptions<SmtpSettings> options, ILogger<EmailService> logger)
{
_settings = options.Value;
_logger = logger;
}
public async Task SendMatchAsync(string? explicitTo, string subject, string body, CancellationToken ct)
{
var to = !string.IsNullOrWhiteSpace(explicitTo) ? explicitTo : _settings.ToEmail;
if (string.IsNullOrWhiteSpace(_settings.Host) || string.IsNullOrWhiteSpace(to))
{
_logger.LogInformation("SMTP is not configured. Skipping CV matcher email.");
return;
}
var message = new MimeMessage();
message.From.Add(MailboxAddress.Parse(_settings.FromEmail));
message.To.Add(MailboxAddress.Parse(to));
message.Subject = subject;
message.Body = new TextPart("plain") { Text = body };
using var client = new SmtpClient();
var secureSocket = _settings.UseStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
await client.ConnectAsync(_settings.Host, _settings.Port, secureSocket, ct);
if (!string.IsNullOrWhiteSpace(_settings.Username))
{
await client.AuthenticateAsync(_settings.Username, _settings.Password, ct);
}
await client.SendAsync(message, ct);
await client.DisconnectAsync(true, ct);
}
}
@@ -1,6 +1,6 @@
namespace Rag.Models namespace Rag.Models.Responses
{ {
public sealed class RagDocumentDetails public sealed class RagDocumentDetailsResponse
{ {
public required string Id { get; init; } public required string Id { get; init; }
public required string DocumentType { get; init; } public required string DocumentType { get; init; }
+5 -4
View File
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Api.Services.Contracts; using Api.Services.Contracts;
using Rag.Models.Requests; using Rag.Models.Requests;
using Rag.Models.Responses;
namespace Api.Controllers; namespace Api.Controllers;
@@ -19,7 +20,7 @@ public sealed class RagController : ControllerBase
[HttpPost("documents")] [HttpPost("documents")]
[RequestSizeLimit(10 * 1024 * 1024)] [RequestSizeLimit(10 * 1024 * 1024)]
public async Task<IActionResult> IndexDocument( public async Task<ActionResult<IndexDocumentResponse>> IndexDocument(
[FromForm] IFormFile? file, [FromForm] IFormFile? file,
[FromForm] string? text, [FromForm] string? text,
[FromForm] string? documentType, [FromForm] string? documentType,
@@ -59,7 +60,7 @@ public sealed class RagController : ControllerBase
} }
[HttpPost("documents/json")] [HttpPost("documents/json")]
public async Task<IActionResult> IndexJsonDocument([FromBody] IndexDocumentRequest request, CancellationToken ct) public async Task<ActionResult<IndexDocumentResponse>> IndexJsonDocument([FromBody] IndexDocumentRequest request, CancellationToken ct)
{ {
try try
{ {
@@ -78,7 +79,7 @@ public sealed class RagController : ControllerBase
} }
[HttpPost("search")] [HttpPost("search")]
public async Task<IActionResult> Search([FromBody] SearchRequest request, CancellationToken ct) public async Task<ActionResult<SearchResponse>> Search([FromBody] SearchRequest request, CancellationToken ct)
{ {
try try
{ {
@@ -96,7 +97,7 @@ public sealed class RagController : ControllerBase
} }
[HttpGet("documents/{id}")] [HttpGet("documents/{id}")]
public async Task<IActionResult> GetDocument(string id, CancellationToken ct) public async Task<ActionResult<RagDocumentDetailsResponse>> GetDocument(string id, CancellationToken ct)
{ {
_logger.LogInformation("Get document request received. DocumentId={DocumentId}", id); _logger.LogInformation("Get document request received. DocumentId={DocumentId}", id);
var document = await _ragService.GetDocumentAsync(id, ct); var document = await _ragService.GetDocumentAsync(id, ct);
+1 -2
View File
@@ -1,4 +1,3 @@
using Rag.Models;
using Rag.Models.Requests; using Rag.Models.Requests;
using Rag.Models.Responses; using Rag.Models.Responses;
@@ -9,5 +8,5 @@ public interface IRagService
Task<IndexDocumentResponse> IndexTextAsync(IndexDocumentRequest request, CancellationToken ct); Task<IndexDocumentResponse> IndexTextAsync(IndexDocumentRequest request, CancellationToken ct);
Task<IndexDocumentResponse> IndexPdfAsync(IFormFile file, string? documentType, string? title, string? sourceUrl, CancellationToken ct); Task<IndexDocumentResponse> IndexPdfAsync(IFormFile file, string? documentType, string? title, string? sourceUrl, CancellationToken ct);
Task<SearchResponse> SearchAsync(SearchRequest request, CancellationToken ct); Task<SearchResponse> SearchAsync(SearchRequest request, CancellationToken ct);
Task<RagDocumentDetails?> GetDocumentAsync(string documentId, CancellationToken ct); Task<RagDocumentDetailsResponse?> GetDocumentAsync(string documentId, CancellationToken ct);
} }
+2 -2
View File
@@ -97,10 +97,10 @@ public sealed class RagService : IRagService
return new SearchResponse { Results = results }; return new SearchResponse { Results = results };
} }
public async Task<RagDocumentDetails?> GetDocumentAsync(string documentId, CancellationToken ct) public async Task<RagDocumentDetailsResponse?> GetDocumentAsync(string documentId, CancellationToken ct)
{ {
var document = await _repository.GetDocumentByIdAsync(documentId, ct); var document = await _repository.GetDocumentByIdAsync(documentId, ct);
return document is null ? null : new RagDocumentDetails return document is null ? null : new RagDocumentDetailsResponse
{ {
Id = document.Id, Id = document.Id,
DocumentType = document.DocumentType, DocumentType = document.DocumentType,