Add XML doc to all service interfaces and implementations (#26)
- Update CLAUDE.md: replace incorrect 'no XML doc on internal code' rule with the correct convention (XML doc on all public methods and non-trivial private/protected helpers) - Restore /// <summary> on FileDownloadController private helpers (HandleRangeRequest, StreamRangeAsync) - Add full XML doc to all service contracts: ICaptchaVerifier, IEmailSender, ICvMatcherService, IJobTextExtractor, IJobTokenService, IDocumentClassifier, IRagService, ITextChunker, ITextExtractor, IEmailTemplateService, ITemplateService - Add /// <summary> and /// <inheritdoc /> to all concrete service classes and their methods: RecaptchaVerifier, EmailApiEmailSender, SmtpEmailDispatcher, CvMatcherService, JobTextExtractor, JobTokenService, RagService, DocumentClassifier, TextChunker, TextExtractor, HtmlJobSearcher, CvSearchEmailSender, CvSearchJobTask, EmailTemplateService, DbTemplateService Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,34 @@ using CvMatcher.Models.Responses;
|
||||
|
||||
namespace Api.Services.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates CV indexing, job matching, and job discovery operations.
|
||||
/// </summary>
|
||||
public interface ICvMatcherService
|
||||
{
|
||||
/// <summary>
|
||||
/// Indexes a CV PDF into the RAG system and returns document metadata.
|
||||
/// Returns cached metadata without re-indexing when the same text hash already exists.
|
||||
/// </summary>
|
||||
/// <param name="file">Uploaded CV PDF file.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Upload response with document ID, hash, and indexing statistics.</returns>
|
||||
Task<CvUploadResponse> UploadCvAsync(IFormFile file, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Scores a CV against a specific job posting URL or pasted description using the LLM.
|
||||
/// Caches the result so repeat requests for the same (CV, job, language) triple are served instantly.
|
||||
/// </summary>
|
||||
/// <param name="request">Match request containing CV document ID, job URL or description, and language preference.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Structured match response with score, summary, strengths, gaps, and recommendations.</returns>
|
||||
Task<JobMatchResponse> MatchJobAsync(MatchJobRequest request, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Searches the RAG index for job documents most similar to the given CV and scores the top candidates.
|
||||
/// </summary>
|
||||
/// <param name="request">Request containing the CV document ID and optional result count limit.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Response with the CV document ID and a list of ranked match results.</returns>
|
||||
Task<FindJobsResponse> FindJobsAsync(FindJobsRequest request, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
namespace Api.Services.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts plain text from a job posting, either from a pasted description or by fetching and parsing a URL.
|
||||
/// </summary>
|
||||
public interface IJobTextExtractor
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns normalised plain text for the job posting.
|
||||
/// Prefers <paramref name="jobDescription"/> when provided; otherwise fetches and strips HTML from <paramref name="jobUrl"/>.
|
||||
/// </summary>
|
||||
/// <param name="jobUrl">URL of the job posting page, used when no description is pasted.</param>
|
||||
/// <param name="jobDescription">Pasted job description text; takes priority over URL fetching.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Normalised plain text, truncated to the configured maximum character limit.</returns>
|
||||
Task<string> ExtractAsync(string? jobUrl, string? jobDescription, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
namespace Api.Services.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Manages one-time job search tokens and the sessions they trigger.
|
||||
/// </summary>
|
||||
public interface IJobTokenService
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new single-use job search token linked to the given CV document and user.
|
||||
/// The token expires after the number of days configured in <c>JobSearch:TokenExpiryDays</c>.
|
||||
/// </summary>
|
||||
/// <param name="cvDocumentId">Identifier of the indexed CV document.</param>
|
||||
/// <param name="email">Email address of the user who will receive the results.</param>
|
||||
/// <param name="language">Preferred language for result emails (e.g. <c>"en"</c>, <c>"ro"</c>).</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The generated token ID, to be embedded in the one-click job search link.</returns>
|
||||
Task<string> CreateTokenAsync(string cvDocumentId, string email, string language, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the token and, if valid, marks it as used and creates a <c>Pending</c> job search session.
|
||||
/// </summary>
|
||||
/// <param name="tokenId">The token ID from the one-click link.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>
|
||||
/// One of the <c>StartJobSearchStatus</c> string constants:
|
||||
/// <c>Started</c>, <c>AlreadyUsed</c>, <c>Expired</c>, or <c>NotFound</c>.
|
||||
/// </returns>
|
||||
Task<string> TriggerStartAsync(string tokenId, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Orchestrates CV upload, RAG indexing, job text extraction, LLM scoring, and result caching.
|
||||
/// </summary>
|
||||
public sealed class CvMatcherService : ICvMatcherService
|
||||
{
|
||||
private readonly IRagApiClient _rag;
|
||||
@@ -35,6 +38,7 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
_settings = options.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CvUploadResponse> UploadCvAsync(IFormFile file, CancellationToken ct)
|
||||
{
|
||||
var response = await _rag.IndexCvPdfAsync(file, ct);
|
||||
@@ -51,6 +55,7 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<FindJobsResponse> FindJobsAsync(FindJobsRequest request, CancellationToken ct)
|
||||
{
|
||||
var cv = await _rag.GetDocumentAsync(request.CvDocumentId, ct) ?? throw new InvalidOperationException("CV document not found.");
|
||||
@@ -78,6 +83,7 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
return new FindJobsResponse { CvDocumentId = request.CvDocumentId, Jobs = jobs };
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<JobMatchResponse> MatchJobAsync(MatchJobRequest request, CancellationToken ct)
|
||||
{
|
||||
if (!request.GdprConsent) throw new InvalidOperationException("GDPR consent is required.");
|
||||
@@ -104,6 +110,11 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
return await ScorePairAsync(cv, jobDocument, matchedChunks, request.Email, NormalizeLanguage(request.Language), ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scores a (CV, job) pair with the LLM.
|
||||
/// Returns a cached result immediately when the same (CV, job, language) triple has been scored before.
|
||||
/// When no evidence chunks are available from the vector search, falls back to the raw job text.
|
||||
/// </summary>
|
||||
private async Task<JobMatchResponse> ScorePairAsync(RagDocumentDetails cv, RagDocumentDetails job, IReadOnlyList<string> evidenceChunks, string? email, string language, CancellationToken ct)
|
||||
{
|
||||
var cached = await _repository.GetMatchAsync(cv.Id, job.Id, language, ct);
|
||||
@@ -138,6 +149,10 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialises the LLM's JSON output into a <see cref="JobMatchResponse"/>.
|
||||
/// Returns a safe fallback response instead of throwing when the JSON cannot be parsed.
|
||||
/// </summary>
|
||||
private static JobMatchResponse ParseResult(string json)
|
||||
{
|
||||
try
|
||||
@@ -158,21 +173,29 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a descriptive search query from the CV text for use in vector similarity search.
|
||||
/// </summary>
|
||||
private static string BuildCvSearchProfile(string cvText)
|
||||
{
|
||||
var text = Limit(cvText, 10000);
|
||||
return $"Candidate profile, skills, technologies, seniority, industry experience, project experience: {text}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts a short job title from the first sentence-like fragment of the job text.
|
||||
/// </summary>
|
||||
private static string ExtractJobTitle(string jobText)
|
||||
{
|
||||
var first = jobText.Split('.', '\n', '\r').Select(x => x.Trim()).FirstOrDefault(x => x.Length is > 8 and < 140);
|
||||
return first ?? "Job description";
|
||||
}
|
||||
|
||||
/// <summary>Returns the base language code, lower-cased, defaulting to <c>"en"</c>.</summary>
|
||||
private static string NormalizeLanguage(string? language) =>
|
||||
string.IsNullOrWhiteSpace(language) ? "en" : language.ToLowerInvariant().Split('-')[0].Trim();
|
||||
|
||||
/// <summary>Maps a language code to its full English name for use in the LLM system prompt.</summary>
|
||||
private static string LanguageName(string language) => language switch
|
||||
{
|
||||
"ro" => "Romanian",
|
||||
@@ -180,5 +203,6 @@ public sealed class CvMatcherService : ICvMatcherService
|
||||
_ => "English"
|
||||
};
|
||||
|
||||
/// <summary>Truncates <paramref name="value"/> to at most <paramref name="max"/> characters.</summary>
|
||||
private static string Limit(string value, int max) => value.Length <= max ? value : value[..max];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Extracts normalised plain text from a job posting, either from a pasted description or by
|
||||
/// fetching and stripping the HTML of the job page URL.
|
||||
/// </summary>
|
||||
public sealed class JobTextExtractor : IJobTextExtractor
|
||||
{
|
||||
private readonly HttpClient _http;
|
||||
@@ -19,6 +23,7 @@ public sealed class JobTextExtractor : IJobTextExtractor
|
||||
_http.DefaultRequestHeaders.UserAgent.ParseAdd("MyAi.ro CV Matcher/1.0");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> ExtractAsync(string? jobUrl, string? jobDescription, CancellationToken ct)
|
||||
{
|
||||
var pasted = Normalize(jobDescription ?? string.Empty);
|
||||
@@ -37,12 +42,14 @@ public sealed class JobTextExtractor : IJobTextExtractor
|
||||
return Limit(Normalize(WebUtility.HtmlDecode(html)));
|
||||
}
|
||||
|
||||
/// <summary>Truncates text to the configured maximum character count.</summary>
|
||||
private string Limit(string value)
|
||||
{
|
||||
var max = Math.Max(4000, _settings.MaxJobTextChars);
|
||||
return value.Length <= max ? value : value[..max];
|
||||
}
|
||||
|
||||
/// <summary>Collapses all whitespace runs to single spaces and trims the result.</summary>
|
||||
private static string Normalize(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||
|
||||
@@ -11,6 +11,9 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Api.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Creates and validates one-time job search tokens, and creates the corresponding search sessions.
|
||||
/// </summary>
|
||||
public sealed class JobTokenService : IJobTokenService
|
||||
{
|
||||
private readonly CvSearchDbContext _db;
|
||||
@@ -30,6 +33,7 @@ public sealed class JobTokenService : IJobTokenService
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> CreateTokenAsync(string cvDocumentId, string email, string language, CancellationToken ct)
|
||||
{
|
||||
var token = new JobSearchTokenEntity
|
||||
@@ -49,6 +53,7 @@ public sealed class JobTokenService : IJobTokenService
|
||||
return token.Id;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> TriggerStartAsync(string tokenId, CancellationToken ct)
|
||||
{
|
||||
var token = await _db.JobSearchTokens.FirstOrDefaultAsync(x => x.Id == tokenId, ct);
|
||||
@@ -86,6 +91,10 @@ public sealed class JobTokenService : IJobTokenService
|
||||
return StartJobSearchStatus.Started;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts up to 10 meaningful keywords from the CV text using simple heuristics (no LLM).
|
||||
/// Takes the first 5 usable lines, splits them into words, strips punctuation, and deduplicates.
|
||||
/// </summary>
|
||||
private static string ExtractKeywords(string cvText)
|
||||
{
|
||||
var lines = cvText
|
||||
|
||||
Reference in New Issue
Block a user