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:
@@ -15,10 +15,12 @@ namespace TarninaHub.Controllers;
|
||||
public class InnovationsController : ControllerBase
|
||||
{
|
||||
private readonly IInnovationService _innovations;
|
||||
private readonly IMatchmakingService _matchmakingService;
|
||||
|
||||
public InnovationsController(IInnovationService innovations)
|
||||
public InnovationsController(IInnovationService innovations, IMatchmakingService matchmakingService)
|
||||
{
|
||||
_innovations = innovations;
|
||||
_matchmakingService = matchmakingService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -56,6 +58,8 @@ public class InnovationsController : ControllerBase
|
||||
|
||||
var created = await _innovations.CreateAsync(request, cancellationToken);
|
||||
|
||||
await _matchmakingService.IndexInnovationAsync(created.Id, cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { innovationId = created.Id }, created);
|
||||
}
|
||||
|
||||
@@ -73,6 +77,11 @@ public class InnovationsController : ControllerBase
|
||||
|
||||
var updated = await _innovations.UpdateAsync(innovationId, request, cancellationToken);
|
||||
|
||||
if (updated is not null)
|
||||
{
|
||||
await _matchmakingService.IndexInnovationAsync(updated.Id, cancellationToken);
|
||||
}
|
||||
|
||||
return updated is null ? NotFound() : Ok(updated);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Security;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/matchmaking")]
|
||||
[Authorize]
|
||||
public class MatchmakingController : ControllerBase
|
||||
{
|
||||
private readonly IMatchmakingService _matchmakingService;
|
||||
|
||||
public MatchmakingController(IMatchmakingService matchmakingService)
|
||||
{
|
||||
_matchmakingService = matchmakingService;
|
||||
}
|
||||
|
||||
[HttpPost("reindex")]
|
||||
[Authorize(Policy = Policies.AdminOnly)]
|
||||
public async Task<ActionResult<MatchmakingReindexResponse>> Reindex(CancellationToken cancellationToken)
|
||||
{
|
||||
var indexed = await _matchmakingService.ReindexAsync(cancellationToken);
|
||||
|
||||
return Ok(new MatchmakingReindexResponse(indexed));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record MatchmakingReindexResponse(int Indexed);
|
||||
@@ -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