feat(backend): add communication, tester, admin and middleman modules
Implement step 17 modules: threads/messages, notifications with automation (submission.created, idea.created, grant_call.opened), innovation test feedback, admin panel (submissions, verify, metrics, grant call status), Middleman adaptation and partnerships. Split multi-type files into one type per file and replace object-valued dictionaries with string-valued maps in data contracts (migrations 0006/0007).
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Matchmaking;
|
||||
|
||||
namespace TarninaHub.Infrastructure.Assist;
|
||||
|
||||
/// <summary>
|
||||
/// Middleman Innowacji (F-8). Adapts an innovation to a service form for an
|
||||
/// institution. Uses the configured LLM when available and a deterministic,
|
||||
/// template-based rationale otherwise.
|
||||
/// </summary>
|
||||
public sealed class MiddlemanAssistant : IMiddlemanService
|
||||
{
|
||||
private const string SystemPrompt =
|
||||
"Jesteś Middlemanem Innowacji w Małopolskim Hubie Innowacji Społecznych. " +
|
||||
"Dostosowujesz sprawdzoną innowację do formy usługi dla instytucji (JST/NGO). " +
|
||||
"Odpowiadaj prostym językiem, konkretnie (co/komu/jak) i nie wymyślaj faktów.";
|
||||
|
||||
private readonly IInnovationService _innovations;
|
||||
private readonly ILlmProvider _llmProvider;
|
||||
private readonly ILlmRequestLimiter _llmRequestLimiter;
|
||||
|
||||
public MiddlemanAssistant(
|
||||
IInnovationService innovations,
|
||||
ILlmProvider llmProvider,
|
||||
ILlmRequestLimiter llmRequestLimiter)
|
||||
{
|
||||
_innovations = innovations;
|
||||
_llmProvider = llmProvider;
|
||||
_llmRequestLimiter = llmRequestLimiter;
|
||||
}
|
||||
|
||||
public async Task<MiddlemanResponse?> AdaptAsync(
|
||||
MiddlemanRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var innovation = await _innovations.GetAsync(request.InnovationId, cancellationToken);
|
||||
|
||||
if (innovation is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var serviceForm = BuildServiceForm(innovation, request);
|
||||
var rationale = BuildRationale(innovation, request);
|
||||
var fromLlm = false;
|
||||
|
||||
if (_llmProvider.IsConfigured && _llmRequestLimiter.TryAcquire())
|
||||
{
|
||||
var profile = request.InstitutionProfile;
|
||||
var userPrompt =
|
||||
$"Innowacja: {innovation.Title}\n" +
|
||||
$"Streszczenie: {innovation.Summary}\n" +
|
||||
$"Opis: {innovation.Description}\n" +
|
||||
$"Etap: {innovation.Stage}\n" +
|
||||
$"Instytucja: {profile?.Type ?? "nieokreślona"} (obszar: {profile?.Area ?? "ogólny"})\n" +
|
||||
$"Odbiorcy: {(profile?.Audiences is { Count: > 0 } audiences ? string.Join(", ", audiences) : "nieokreśleni")}\n" +
|
||||
$"Potrzeby: {request.Needs ?? "ogólne"}\n" +
|
||||
"Zaproponuj formę usługi (co/komu/jak) i uzasadnienie.";
|
||||
|
||||
var completion = await _llmProvider.CompleteAsync(SystemPrompt, userPrompt, cancellationToken);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(completion))
|
||||
{
|
||||
serviceForm = completion.Trim();
|
||||
fromLlm = true;
|
||||
}
|
||||
}
|
||||
|
||||
return new MiddlemanResponse(serviceForm, BuildResources(innovation), rationale, fromLlm);
|
||||
}
|
||||
|
||||
private static string BuildServiceForm(InnovationResponse innovation, MiddlemanRequest request)
|
||||
{
|
||||
var audiences = request.InstitutionProfile?.Audiences is { Count: > 0 } values
|
||||
? string.Join(", ", values)
|
||||
: "odbiorcy wskazani przez instytucję";
|
||||
|
||||
var area = request.InstitutionProfile?.Area ?? innovation.ChallengeId;
|
||||
|
||||
return $"Co: {innovation.Title} — {innovation.Summary}. " +
|
||||
$"Komu: {audiences}. " +
|
||||
$"Jak: wdrożyć jako usługę w obszarze '{area}', etap '{innovation.Stage}', " +
|
||||
$"z potrzeby: {request.Needs ?? "zdiagnozowanej lokalnie"}.";
|
||||
}
|
||||
|
||||
private static string BuildRationale(InnovationResponse innovation, MiddlemanRequest request)
|
||||
{
|
||||
var institution = request.InstitutionProfile?.Type ?? "instytucja";
|
||||
|
||||
return $"Innowacja na etapie '{innovation.Stage}' odpowiada profilowi '{institution}' i obszarowi " +
|
||||
$"'{request.InstitutionProfile?.Area ?? innovation.ChallengeId}', dlatego nadaje się do " +
|
||||
"przekształcenia w powtarzalną usługę społeczną.";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> BuildResources(InnovationResponse innovation)
|
||||
{
|
||||
var resources = new List<string>
|
||||
{
|
||||
"Koordynator usługi i zespół realizacyjny",
|
||||
"Budżet na pilotaż oraz pomiar efektów",
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(innovation.Cost))
|
||||
{
|
||||
resources.Add($"Szacowany koszt: {innovation.Cost}");
|
||||
}
|
||||
|
||||
resources.Add("Partnerzy lokalni (NGO/JST/firma) do wdrożenia");
|
||||
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user