feat(backend): add semantic matchmaking with embedding pipeline
Add a pluggable embedding provider with an offline deterministic default, vector indexing for problems and innovations, cosine semantic ranking, similar cases and solution suggestions, area filtering and an admin reindex endpoint. Refresh the index when innovations or submissions change.
This commit is contained in:
@@ -16,10 +16,14 @@ namespace TarninaHub.Controllers;
|
||||
public class SubmissionsController : ControllerBase
|
||||
{
|
||||
private readonly ISubmissionService _submissionService;
|
||||
private readonly IMatchmakingService _matchmakingService;
|
||||
|
||||
public SubmissionsController(ISubmissionService submissionService)
|
||||
public SubmissionsController(
|
||||
ISubmissionService submissionService,
|
||||
IMatchmakingService matchmakingService)
|
||||
{
|
||||
_submissionService = submissionService;
|
||||
_matchmakingService = matchmakingService;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -36,6 +40,8 @@ public class SubmissionsController : ControllerBase
|
||||
|
||||
var created = await _submissionService.CreateAsync(userId, request, cancellationToken);
|
||||
|
||||
await _matchmakingService.IndexSubmissionAsync(created.Id, cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { submissionId = created.Id }, created);
|
||||
}
|
||||
|
||||
@@ -132,6 +138,75 @@ public class SubmissionsController : ControllerBase
|
||||
return updated is null ? NotFound() : Ok(updated);
|
||||
}
|
||||
|
||||
[HttpPost("{submissionId}/match")]
|
||||
public async Task<ActionResult<MatchResponse>> Match(
|
||||
string submissionId,
|
||||
[FromBody] MatchRequest? request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var access = await AuthorizeSubmissionAccessAsync(submissionId, cancellationToken);
|
||||
|
||||
if (access is not null)
|
||||
{
|
||||
return access;
|
||||
}
|
||||
|
||||
var result = await _matchmakingService.MatchAsync(
|
||||
submissionId,
|
||||
request ?? new MatchRequest(null, null),
|
||||
cancellationToken);
|
||||
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpGet("{submissionId}/matches")]
|
||||
public async Task<ActionResult<MatchResponse>> Matches(
|
||||
string submissionId,
|
||||
[FromQuery] string? challengeId,
|
||||
[FromQuery] int? maxResults,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var access = await AuthorizeSubmissionAccessAsync(submissionId, cancellationToken);
|
||||
|
||||
if (access is not null)
|
||||
{
|
||||
return access;
|
||||
}
|
||||
|
||||
var result = await _matchmakingService.MatchAsync(
|
||||
submissionId,
|
||||
new MatchRequest(maxResults, challengeId),
|
||||
cancellationToken);
|
||||
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
private async Task<ActionResult?> AuthorizeSubmissionAccessAsync(
|
||||
string submissionId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var submission = await _submissionService.GetAsync(submissionId, cancellationToken);
|
||||
|
||||
if (submission is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!IsStaff() && !string.Equals(submission.AuthorId, userId, StringComparison.Ordinal))
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool IsStaff() =>
|
||||
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user