@@ -1,6 +1,7 @@
|
||||
using CvMatcher.Models.Requests;
|
||||
using Api.Services.Contracts;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using CvMatcher.Models.Responses;
|
||||
|
||||
namespace Api.Controllers;
|
||||
|
||||
@@ -19,7 +20,7 @@ public sealed class CvController : ControllerBase
|
||||
|
||||
[HttpPost("upload")]
|
||||
[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
|
||||
{
|
||||
@@ -37,14 +38,14 @@ public sealed class CvController : ControllerBase
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
_logger.LogInformation("Find jobs request received. CvDocumentId={CvDocumentId}, TopK={TopK}", request.CvDocumentId, request.TopK);
|
||||
var result = await _service.FindJobsAsync(request, ct);
|
||||
_logger.LogInformation("Find jobs completed. CvDocumentId={CvDocumentId}, ResultCount={ResultCount}", request.CvDocumentId, result.Jobs.Count);
|
||||
return Ok(result);
|
||||
return result;
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
@@ -54,7 +55,7 @@ public sealed class CvController : ControllerBase
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
@@ -62,7 +63,7 @@ public sealed class CvController : ControllerBase
|
||||
request.CvDocumentId, !string.IsNullOrWhiteSpace(request.JobUrl), !string.IsNullOrWhiteSpace(request.JobDescription), !string.IsNullOrWhiteSpace(request.Email));
|
||||
var result = await _service.MatchJobAsync(request, ct);
|
||||
_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)
|
||||
{
|
||||
|
||||
@@ -97,7 +97,6 @@ try
|
||||
?? throw new InvalidOperationException("Connection string 'CvMatcherDb' is missing.")));
|
||||
builder.Services.AddScoped<IMatcherRepository, EfMatcherRepository>();
|
||||
builder.Services.AddScoped<ICvMatcherService, CvMatcherService>();
|
||||
builder.Services.AddSingleton<IEmailService, EmailService>();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Api.Services.Contracts;
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
Task SendMatchAsync(string? explicitTo, string subject, string body, CancellationToken ct);
|
||||
}
|
||||
@@ -16,7 +16,6 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
private readonly IJobTextExtractor _jobTextExtractor;
|
||||
private readonly IMatcherAiClient _ai;
|
||||
private readonly IMatcherRepository _repository;
|
||||
private readonly IEmailService _email;
|
||||
private readonly MatcherSettings _settings;
|
||||
|
||||
public CvMatcherService(
|
||||
@@ -24,14 +23,12 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
IJobTextExtractor jobTextExtractor,
|
||||
IMatcherAiClient ai,
|
||||
IMatcherRepository repository,
|
||||
IEmailService email,
|
||||
IOptions<MatcherSettings> options)
|
||||
{
|
||||
_rag = rag;
|
||||
_jobTextExtractor = jobTextExtractor;
|
||||
_ai = ai;
|
||||
_repository = repository;
|
||||
_email = email;
|
||||
_settings = options.Value;
|
||||
}
|
||||
|
||||
@@ -138,11 +135,11 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
result.Cached = false;
|
||||
await _repository.SaveMatchAsync(cv.Id, job.Id, result, ct);
|
||||
|
||||
await _email.SendMatchAsync(
|
||||
email,
|
||||
$"MyAi.ro CV Match: {result.Score}% - {job.Title}",
|
||||
BuildEmailBody(cv, job, result),
|
||||
ct);
|
||||
//await _email.SendMatchAsync(
|
||||
// email,
|
||||
// $"MyAi.ro CV Match: {result.Score}% - {job.Title}",
|
||||
// BuildEmailBody(cv, job, result),
|
||||
// ct);
|
||||
|
||||
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 BuildEmailBody(RagDocumentDetails cv, RagDocumentDetails job, JobMatchResponse result) => $"""
|
||||
CV Matcher result
|
||||
//private static string BuildEmailBody(RagDocumentDetails cv, RagDocumentDetails job, JobMatchResponse result) => $"""
|
||||
// CV Matcher result
|
||||
|
||||
CV: {cv.Title}
|
||||
Job: {job.Title}
|
||||
Job URL: {job.SourceUrl ?? "N/A"}
|
||||
Score: {result.Score}%
|
||||
// CV: {cv.Title}
|
||||
// Job: {job.Title}
|
||||
// Job URL: {job.SourceUrl ?? "N/A"}
|
||||
// Score: {result.Score}%
|
||||
|
||||
Summary:
|
||||
{result.Summary}
|
||||
// Summary:
|
||||
// {result.Summary}
|
||||
|
||||
Strengths:
|
||||
- {string.Join("\n- ", result.Strengths)}
|
||||
// Strengths:
|
||||
// - {string.Join("\n- ", result.Strengths)}
|
||||
|
||||
Gaps:
|
||||
- {string.Join("\n- ", result.Gaps)}
|
||||
// Gaps:
|
||||
// - {string.Join("\n- ", result.Gaps)}
|
||||
|
||||
Recommendations:
|
||||
- {string.Join("\n- ", result.Recommendations)}
|
||||
""";
|
||||
// Recommendations:
|
||||
// - {string.Join("\n- ", result.Recommendations)}
|
||||
// """;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user