@@ -1,14 +1,11 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Shared.Models.Requests;
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
|
|
||||||
namespace Models.Requests
|
namespace Models.Requests
|
||||||
{
|
{
|
||||||
public sealed class UploadCvRequest
|
public sealed class UploadCvRequest : UploadFileRequest
|
||||||
{
|
{
|
||||||
[Required]
|
|
||||||
public IFormFile Cv { get; set; } = default!;
|
|
||||||
|
|
||||||
public bool GdprConsent { get; set; }
|
public bool GdprConsent { get; set; }
|
||||||
public string? CaptchaToken { get; set; }
|
public string? CaptchaToken { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,4 +11,8 @@
|
|||||||
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
|
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\shared-models\shared-models.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Refit;
|
|
||||||
using Models.Requests;
|
|
||||||
using CvMatcher.Models.Responses;
|
using CvMatcher.Models.Responses;
|
||||||
|
using Models.Requests;
|
||||||
|
using Refit;
|
||||||
|
|
||||||
namespace Api.Clients.Api.Contracts;
|
namespace Api.Clients.Api.Contracts;
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ public interface ICvMatcherApi
|
|||||||
{
|
{
|
||||||
[Multipart]
|
[Multipart]
|
||||||
[Post("/api/cv/upload")]
|
[Post("/api/cv/upload")]
|
||||||
Task<CvUploadResponse> Upload([AliasAs("cv")] StreamPart cv, [AliasAs("gdprConsent")] bool gdprConsent);
|
Task<CvUploadResponse> Upload([AliasAs("file")] StreamPart file, CancellationToken ct);
|
||||||
|
|
||||||
[Post("/api/cv/match-job")]
|
[Post("/api/cv/match-job")]
|
||||||
Task<JobMatchResponse> MatchJob([Body] JobMatchRequest request);
|
Task<JobMatchResponse> MatchJob([Body] JobMatchRequest request, CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,12 +47,12 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
[FromForm] UploadCvRequest request,
|
[FromForm] UploadCvRequest request,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (request.Cv is null)
|
if (request.File is null)
|
||||||
{
|
{
|
||||||
return BadRequest(new { error = "Missing CV PDF." });
|
return BadRequest(new { error = "Missing CV PDF." });
|
||||||
}
|
}
|
||||||
|
|
||||||
var cv = request.Cv;
|
var cv = request.File;
|
||||||
var gdprConsent = request.GdprConsent;
|
var gdprConsent = request.GdprConsent;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -65,12 +65,14 @@ public sealed class CvMatcherController : ControllerBase
|
|||||||
return BadRequest(new { error = "Captcha verification failed." });
|
return BadRequest(new { error = "Captcha verification failed." });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!gdprConsent) throw new InvalidOperationException("GDPR consent is required.");
|
||||||
|
|
||||||
_logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}",
|
_logger.LogInformation("Proxying CV upload to cv-matcher-api. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}",
|
||||||
cv.FileName, cv.Length, gdprConsent);
|
cv.FileName, cv.Length, gdprConsent);
|
||||||
|
|
||||||
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");
|
||||||
var res = await _cvApi.Upload(part, gdprConsent);
|
var res = await _cvApi.Upload(part, ct);
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
@@ -114,7 +116,7 @@ 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);
|
var res = await _cvApi.MatchJob(request, ct);
|
||||||
return Ok(res);
|
return Ok(res);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using CvMatcher.Models.Requests;
|
|||||||
using Api.Services.Contracts;
|
using Api.Services.Contracts;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using CvMatcher.Models.Responses;
|
using CvMatcher.Models.Responses;
|
||||||
|
using Shared.Models.Requests;
|
||||||
|
|
||||||
namespace Api.Controllers;
|
namespace Api.Controllers;
|
||||||
|
|
||||||
@@ -20,13 +21,13 @@ public sealed class CvController : ControllerBase
|
|||||||
|
|
||||||
[HttpPost("upload")]
|
[HttpPost("upload")]
|
||||||
[RequestSizeLimit(10 * 1024 * 1024)]
|
[RequestSizeLimit(10 * 1024 * 1024)]
|
||||||
public async Task<ActionResult<CvUploadResponse>> Upload([FromForm(Name = "cv")] IFormFile? cv, [FromForm] bool gdprConsent, CancellationToken ct)
|
public async Task<ActionResult<CvUploadResponse>> Upload([FromForm] UploadFileRequest request, CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (cv is null) return BadRequest(new { error = "Missing CV PDF." });
|
if (request.File is null) return BadRequest(new { error = "Missing CV PDF." });
|
||||||
_logger.LogInformation("CV upload received. FileName={FileName}, Size={SizeBytes}, GdprConsent={GdprConsent}", cv.FileName, cv.Length, gdprConsent);
|
_logger.LogInformation("CV upload received. FileName={FileName}, Size={SizeBytes}", request.File.FileName, request.File.Length);
|
||||||
var result = await _service.UploadCvAsync(cv, gdprConsent, ct);
|
var result = await _service.UploadCvAsync(request.File, ct);
|
||||||
_logger.LogInformation("CV upload processed. CvDocumentId={CvDocumentId}, Cached={Cached}", result.DocumentId, result.Cached);
|
_logger.LogInformation("CV upload processed. CvDocumentId={CvDocumentId}, Cached={Cached}", result.DocumentId, result.Cached);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Api.Services.Contracts;
|
|||||||
|
|
||||||
public interface ICvMatcherService
|
public interface ICvMatcherService
|
||||||
{
|
{
|
||||||
Task<CvUploadResponse> UploadCvAsync(IFormFile file, bool gdprConsent, CancellationToken ct);
|
Task<CvUploadResponse> UploadCvAsync(IFormFile file, CancellationToken ct);
|
||||||
Task<JobMatchResponse> MatchJobAsync(MatchJobRequest request, CancellationToken ct);
|
Task<JobMatchResponse> MatchJobAsync(MatchJobRequest request, CancellationToken ct);
|
||||||
Task<FindJobsResponse> FindJobsAsync(FindJobsRequest request, CancellationToken ct);
|
Task<FindJobsResponse> FindJobsAsync(FindJobsRequest request, CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,9 +32,8 @@ public sealed class CvMatcherService : ICvMatcherService
|
|||||||
_settings = options.Value;
|
_settings = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CvUploadResponse> UploadCvAsync(IFormFile file, bool gdprConsent, CancellationToken ct)
|
public async Task<CvUploadResponse> UploadCvAsync(IFormFile file, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (!gdprConsent) throw new InvalidOperationException("GDPR consent is required.");
|
|
||||||
var response = await _rag.IndexCvPdfAsync(file, ct);
|
var response = await _rag.IndexCvPdfAsync(file, ct);
|
||||||
return new CvUploadResponse
|
return new CvUploadResponse
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Rag.Models.Requests
|
namespace Rag.Models.Requests
|
||||||
{
|
{
|
||||||
public sealed class IndexDocumentRequest
|
public class IndexDocumentRequest
|
||||||
{
|
{
|
||||||
public string? Text { get; set; }
|
public string? Text { get; set; }
|
||||||
public string? SourceUrl { get; set; }
|
public string? SourceUrl { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Rag.Models.Requests
|
||||||
|
{
|
||||||
|
public sealed class IndexDocumentUploadRequest: IndexDocumentRequest
|
||||||
|
{
|
||||||
|
public IFormFile? File { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,21 +21,17 @@ public sealed class RagController : ControllerBase
|
|||||||
[HttpPost("documents")]
|
[HttpPost("documents")]
|
||||||
[RequestSizeLimit(10 * 1024 * 1024)]
|
[RequestSizeLimit(10 * 1024 * 1024)]
|
||||||
public async Task<ActionResult<IndexDocumentResponse>> IndexDocument(
|
public async Task<ActionResult<IndexDocumentResponse>> IndexDocument(
|
||||||
[FromForm] IFormFile? file,
|
[FromForm] IndexDocumentUploadRequest request,
|
||||||
[FromForm] string? text,
|
|
||||||
[FromForm] string? documentType,
|
|
||||||
[FromForm] string? title,
|
|
||||||
[FromForm] string? sourceUrl,
|
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Index document request received. HasFile={HasFile}, DocumentType={DocumentType}, Title={Title}, SourceUrl={SourceUrl}",
|
_logger.LogInformation("Index document request received. HasFile={HasFile}, DocumentType={DocumentType}, Title={Title}, SourceUrl={SourceUrl}",
|
||||||
file is not null, documentType, title, sourceUrl);
|
request.File is not null, request.DocumentType, request.Title, request.SourceUrl);
|
||||||
|
|
||||||
if (file is not null)
|
if (request.File is not null)
|
||||||
{
|
{
|
||||||
var result = await _ragService.IndexPdfAsync(file, documentType, title, sourceUrl, ct);
|
var result = await _ragService.IndexPdfAsync(request.File, request.DocumentType, request.Title, request.SourceUrl, ct);
|
||||||
_logger.LogInformation("Indexed PDF document. DocumentId={DocumentId}, DocumentType={DocumentType}, Chunks={Chunks}, Cached={Cached}",
|
_logger.LogInformation("Indexed PDF document. DocumentId={DocumentId}, DocumentType={DocumentType}, Chunks={Chunks}, Cached={Cached}",
|
||||||
result.DocumentId, result.DocumentType, result.Chunks, result.Cached);
|
result.DocumentId, result.DocumentType, result.Chunks, result.Cached);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
@@ -43,10 +39,10 @@ public sealed class RagController : ControllerBase
|
|||||||
|
|
||||||
var textResult = await _ragService.IndexTextAsync(new IndexDocumentRequest
|
var textResult = await _ragService.IndexTextAsync(new IndexDocumentRequest
|
||||||
{
|
{
|
||||||
Text = text,
|
Text = request.Text,
|
||||||
DocumentType = documentType,
|
DocumentType = request.DocumentType,
|
||||||
Title = title,
|
Title = request.Title,
|
||||||
SourceUrl = sourceUrl
|
SourceUrl = request.SourceUrl
|
||||||
}, ct);
|
}, ct);
|
||||||
_logger.LogInformation("Indexed text document. DocumentId={DocumentId}, DocumentType={DocumentType}, Chunks={Chunks}, Cached={Cached}",
|
_logger.LogInformation("Indexed text document. DocumentId={DocumentId}, DocumentType={DocumentType}, Chunks={Chunks}, Cached={Cached}",
|
||||||
textResult.DocumentId, textResult.DocumentType, textResult.Chunks, textResult.Cached);
|
textResult.DocumentId, textResult.DocumentType, textResult.Chunks, textResult.Cached);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Shared.Models.Requests
|
||||||
|
{
|
||||||
|
public class UploadFileRequest
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
public IFormFile File { get; set; } = default!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,8 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user