Files
Rafał Tyński fa1199539b 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).
2026-10-04 03:49:45 +02:00

114 lines
4.4 KiB
C#

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;
}
}