diff --git a/.env.sample b/.env.sample index 034e914e..b0118127 100644 --- a/.env.sample +++ b/.env.sample @@ -4,6 +4,14 @@ POSTGRES_PORT=5432 POSTGRES_DB=yavsc POSTGRES_USER=yavsc POSTGRES_PASSWORD=lame-YAVSC_CONNECTION_PASSWORD -DESTDIR=/srv/www/yavsc - ASPNETCORE_ConnectionStrings__YavscConnection=Server=$POSTGRES_HOST;Port=$POSTGRES_PORT;Database=$POSTGRES_DB;Username=$POSTGRES_USER;Password=$POSTGRES_PASSWORD; + +ANTHROPIC_API_KEY= +MODERATION_AUTO_REJECT_THRESHOLD=0.9 +MODERATION_AUTO_APPROVE_THRESHOLD=0.7 +# Limite par appel +ANTHROPIC_MAX_TOKENS=256 # la modération n'a pas besoin de plus + +# Choisis le bon modèle selon l'usage +# - Modération : claude-haiku-4-5 (rapide, pas cher) +# - Aide rédaction devis : claude-sonnet-4-6 (meilleur équilibre) diff --git a/Directory.Packages.props b/Directory.Packages.props index 75b895be..0d535731 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,7 @@ true + @@ -63,10 +64,10 @@ - + - + - + \ No newline at end of file diff --git a/src/Yavsc.Abstract/Interfaces/IModerationService.cs b/src/Yavsc.Abstract/Interfaces/IModerationService.cs new file mode 100644 index 00000000..e02422c5 --- /dev/null +++ b/src/Yavsc.Abstract/Interfaces/IModerationService.cs @@ -0,0 +1,8 @@ +using Yavsc.Moderation; + +namespace Yavsc.Abstract.Interfaces; + +public interface IModerationService +{ + Task ModerateAsync(string content, string context); +} diff --git a/src/Yavsc.Abstract/Moderation/ModerationAction.cs b/src/Yavsc.Abstract/Moderation/ModerationAction.cs new file mode 100644 index 00000000..9e57b501 --- /dev/null +++ b/src/Yavsc.Abstract/Moderation/ModerationAction.cs @@ -0,0 +1,3 @@ +namespace Yavsc.Moderation; + +public enum ModerationAction { Approved, Rejected, NeedsReview } diff --git a/src/Yavsc.Abstract/Moderation/ModerationResult.cs b/src/Yavsc.Abstract/Moderation/ModerationResult.cs new file mode 100644 index 00000000..2d3907b3 --- /dev/null +++ b/src/Yavsc.Abstract/Moderation/ModerationResult.cs @@ -0,0 +1,7 @@ +namespace Yavsc.Moderation; + +public record ModerationResult( + ModerationAction Action, + string Reason, + float ConfidenceScore +); diff --git a/src/Yavsc.Org/Program.cs b/src/Yavsc.Org/Program.cs index ea12bc21..2803dbad 100644 --- a/src/Yavsc.Org/Program.cs +++ b/src/Yavsc.Org/Program.cs @@ -1,5 +1,7 @@ +using Anthropic.SDK; using Microsoft.AspNetCore; +using Yavsc.Abstract.Interfaces; using Yavsc.Extensions; namespace Yavsc @@ -9,12 +11,29 @@ namespace Yavsc public static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); - + + // Anthropic client + builder.Services.AddSingleton(_ => + new AnthropicClient( + new APIAuthentication( + builder.Configuration["ANTHROPIC_API_KEY"] + ?? throw new InvalidOperationException("ANTHROPIC_API_KEY manquante") + ) + ) + ); + + // Service de modération + if (builder.Environment.IsDevelopment()) + builder.Services.AddScoped(); + else + builder.Services.AddScoped(); + builder.Configuration - .AddJsonFile("appsettings.json") - .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables() - .Build(); + .AddJsonFile("appsettings.json") + .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables() + .Build(); + var app = await builder.ConfigureWebAppServices().ConfigurePipeline(); app.Run(); } diff --git a/src/Yavsc.Org/appsettings.json b/src/Yavsc.Org/appsettings.json index 81445f5f..e5a6c624 100644 --- a/src/Yavsc.Org/appsettings.json +++ b/src/Yavsc.Org/appsettings.json @@ -77,5 +77,9 @@ }, "Kyc": { "HmacSecret": "*** via dotnet user-secrets ou variable d'environnement ***" + }, + "Moderation": { + "AutoApproveThreshold": 0.7, + "AutoRejectThreshold": 0.9 } } diff --git a/src/Yavsc.Server/Services/ClaudeModerationService.cs b/src/Yavsc.Server/Services/ClaudeModerationService.cs new file mode 100644 index 00000000..0d6260ae --- /dev/null +++ b/src/Yavsc.Server/Services/ClaudeModerationService.cs @@ -0,0 +1,90 @@ +using Yavsc.Moderation; +using Yavsc.Abstract.Interfaces; +using Anthropic.SDK; +using Anthropic.SDK.Messaging; +using Anthropic.SDK.Constants; +using Anthropic.SDK.Models; +using Newtonsoft.Json; +using Microsoft.Extensions.Configuration; +using System.Text.Json; +public class ClaudeModerationService : IModerationService +{ + private readonly AnthropicClient _client; + private readonly float _autoApproveThreshold; + private readonly float _autoRejectThreshold; + + public ClaudeModerationService( + AnthropicClient client, + IConfiguration config) + { + _client = client; + _autoApproveThreshold = config.GetValue( + "Moderation:AutoApproveThreshold", 0.7f); + _autoRejectThreshold = config.GetValue( + "Moderation:AutoRejectThreshold", 0.9f); + } + + public async Task ModerateAsync(string content, string context) + { + var prompt = $$""" + Tu es un modérateur pour une plateforme de mise en relation prestataires/clients. + Contexte : {{context}} + Contenu à modérer : {{content}} + + Réponds UNIQUEMENT en JSON : + { + "action": "approved" | "rejected" | "needs_review", + "reason": "explication courte", + "confidence": 0.0 à 1.0 + } + """; + + var response = await _client.Messages.GetClaudeMessageAsync( + new MessageParameters + { + Model = AnthropicModels.Claude45Haiku, // Haiku : rapide et économique + MaxTokens = 256, + Messages = [new Message(RoleType.User, prompt)] + }); + + var text = response.Content + .OfType() + .FirstOrDefault()?.Text ?? string.Empty; + + return ParseResult(text); + } + + private ModerationResult ParseResult(string json) + { + try + { + var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + + var actionStr = root.GetProperty("action").GetString(); + var reason = root.GetProperty("reason").GetString() ?? string.Empty; + var confidence = root.GetProperty("confidence").GetSingle(); + + var action = actionStr switch + { + "approved" => confidence >= _autoApproveThreshold + ? ModerationAction.Approved + : ModerationAction.NeedsReview, + "rejected" => confidence >= _autoRejectThreshold + ? ModerationAction.Rejected + : ModerationAction.NeedsReview, + "needs_review" => ModerationAction.NeedsReview, + _ => ModerationAction.NeedsReview + }; + + return new ModerationResult(action, reason, confidence); + } + catch + { + return new ModerationResult( + ModerationAction.NeedsReview, + "Erreur de parsing de la réponse", + 0f); + } + } +} \ No newline at end of file diff --git a/src/Yavsc.Server/Services/MockModerationService.cs b/src/Yavsc.Server/Services/MockModerationService.cs new file mode 100644 index 00000000..3c291a2f --- /dev/null +++ b/src/Yavsc.Server/Services/MockModerationService.cs @@ -0,0 +1,11 @@ +using Yavsc.Abstract.Interfaces; +using Yavsc.Moderation; + +public class MockModerationService : IModerationService +{ + public Task ModerateAsync(string content, string context) + => Task.FromResult(new ModerationResult( + ModerationAction.Approved, + "Mock modération — API non configurée", + 1.0f)); +} \ No newline at end of file diff --git a/src/Yavsc.Server/Yavsc.Server.csproj b/src/Yavsc.Server/Yavsc.Server.csproj index 66b41ba5..a04afbb6 100644 --- a/src/Yavsc.Server/Yavsc.Server.csproj +++ b/src/Yavsc.Server/Yavsc.Server.csproj @@ -8,6 +8,7 @@ https://github.com/pazof/yavsc +