feat: изменил логику анализа отзывов
Backend CI / build-and-test (push) Failing after 14m19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 12m5s
Frontend CI / build-and-check (push) Failing after 17m58s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 10m11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 11m3s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
Backend CI / build-and-test (push) Failing after 14m19s
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Failing after 12m5s
Frontend CI / build-and-check (push) Failing after 17m58s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Failing after 10m11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Failing after 11m3s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Failing after 14m58s
This commit is contained in:
@@ -21,6 +21,7 @@ public class ReviewConfiguration : IEntityTypeConfiguration<Review>
|
||||
builder.Property(r => r.QualityScore).HasColumnName("quality_score");
|
||||
builder.Property(r => r.IsInformative).HasColumnName("is_informative");
|
||||
builder.Property(r => r.LlmTags).HasColumnName("llm_tags");
|
||||
builder.Property(r => r.LlmRawOutput).HasColumnName("llm_raw_output");
|
||||
builder.Property(r => r.CreatedAt).HasColumnName("created_at").HasDefaultValueSql("NOW()");
|
||||
builder.Property(r => r.UpdatedAt).HasColumnName("updated_at").HasDefaultValueSql("NOW()");
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using UniVerse.Application.Interfaces;
|
||||
@@ -49,11 +50,37 @@ public class LlmClient : ILlmClient
|
||||
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
var content = json.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString()!;
|
||||
var analysis = JsonSerializer.Deserialize<LlmRawResponse>(content,
|
||||
var analysisJson = NormalizeJsonContent(content);
|
||||
var analysis = JsonSerializer.Deserialize<LlmRawResponse>(analysisJson,
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
|
||||
|
||||
return new LlmReviewAnalysis(analysis.QualityScore, analysis.Sentiment, analysis.Tags, analysis.IsInformative);
|
||||
return new LlmReviewAnalysis(
|
||||
Math.Clamp(analysis.QualityScore, 0, 1),
|
||||
analysis.Sentiment ?? "",
|
||||
analysis.Tags ?? [],
|
||||
analysis.IsInformative,
|
||||
content);
|
||||
}
|
||||
|
||||
private record LlmRawResponse(double QualityScore, string Sentiment, string[] Tags, bool IsInformative);
|
||||
private static string NormalizeJsonContent(string content)
|
||||
{
|
||||
var trimmed = content.Trim();
|
||||
if (!trimmed.StartsWith("```", StringComparison.Ordinal))
|
||||
return trimmed;
|
||||
|
||||
var firstNewLine = trimmed.IndexOf('\n');
|
||||
if (firstNewLine < 0)
|
||||
return trimmed;
|
||||
|
||||
var lastFence = trimmed.LastIndexOf("```", StringComparison.Ordinal);
|
||||
return lastFence > firstNewLine
|
||||
? trimmed[(firstNewLine + 1)..lastFence].Trim()
|
||||
: trimmed[(firstNewLine + 1)..].Trim();
|
||||
}
|
||||
|
||||
private record LlmRawResponse(
|
||||
[property: JsonPropertyName("quality_score")] double QualityScore,
|
||||
string? Sentiment,
|
||||
string[]? Tags,
|
||||
[property: JsonPropertyName("is_informative")] bool IsInformative);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace UniVerse.Infrastructure.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ReviewLlmRawOutput : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "llm_raw_output",
|
||||
table: "reviews",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "llm_raw_output",
|
||||
table: "reviews");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,6 +521,10 @@ namespace UniVerse.Infrastructure.Migrations
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("llm_status");
|
||||
|
||||
b.Property<string>("LlmRawOutput")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("llm_raw_output");
|
||||
|
||||
b.PrimitiveCollection<string[]>("LlmTags")
|
||||
.HasColumnType("text[]")
|
||||
.HasColumnName("llm_tags");
|
||||
|
||||
@@ -31,10 +31,10 @@ public class LlmAnalysisService : ILlmAnalysisService
|
||||
var result = await _llm.AnalyzeReviewAsync(review.Text ?? "", context);
|
||||
|
||||
review.QualityScore = result.QualityScore;
|
||||
review.Sentiment = Enum.TryParse<ReviewSentiment>(result.Sentiment, true, out var s)
|
||||
? s : ReviewSentiment.Neutral;
|
||||
review.Sentiment = ParseSentiment(result.Sentiment);
|
||||
review.LlmTags = result.Tags;
|
||||
review.IsInformative = result.IsInformative;
|
||||
review.LlmRawOutput = result.RawOutput;
|
||||
review.LlmStatus = ReviewLlmStatus.Analyzed;
|
||||
review.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
@@ -53,14 +53,16 @@ public class LlmAnalysisService : ILlmAnalysisService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ProcessPendingReviewsAsync()
|
||||
private static ReviewSentiment ParseSentiment(string value)
|
||||
{
|
||||
var pending = await _db.Reviews
|
||||
.Where(r => r.LlmStatus == ReviewLlmStatus.Pending)
|
||||
.OrderBy(r => r.CreatedAt).Take(10)
|
||||
.Select(r => r.Id).ToListAsync();
|
||||
|
||||
foreach (var id in pending)
|
||||
await AnalyzeReviewAsync(id);
|
||||
var normalized = value.Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"positive" or "положительный" or "положительная" or "позитивный" or "позитивная" => ReviewSentiment.Positive,
|
||||
"negative" or "отрицательный" or "отрицательная" or "негативный" or "негативная" => ReviewSentiment.Negative,
|
||||
"neutral" or "нейтральный" or "нейтральная" => ReviewSentiment.Neutral,
|
||||
_ when Enum.TryParse<ReviewSentiment>(value, true, out var sentiment) => sentiment,
|
||||
_ => ReviewSentiment.Neutral
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,16 @@ public class ReviewService : IReviewService
|
||||
{
|
||||
private readonly AppDbContext _db;
|
||||
private readonly IGamificationService _gamification;
|
||||
private readonly IReviewAnalysisQueue _reviewAnalysisQueue;
|
||||
|
||||
public ReviewService(AppDbContext db, IGamificationService gamification)
|
||||
public ReviewService(
|
||||
AppDbContext db,
|
||||
IGamificationService gamification,
|
||||
IReviewAnalysisQueue reviewAnalysisQueue)
|
||||
{
|
||||
_db = db;
|
||||
_gamification = gamification;
|
||||
_reviewAnalysisQueue = reviewAnalysisQueue;
|
||||
}
|
||||
|
||||
private IQueryable<Review> BaseQuery() => _db.Reviews
|
||||
@@ -38,6 +43,7 @@ public class ReviewService : IReviewService
|
||||
_db.Reviews.Add(review);
|
||||
await _db.SaveChangesAsync();
|
||||
await _gamification.CheckAndAwardAchievementsAsync(userId);
|
||||
await _reviewAnalysisQueue.EnqueueAsync(review.Id);
|
||||
var full = await BaseQuery().FirstAsync(r => r.Id == review.Id);
|
||||
return full.ToDto();
|
||||
}
|
||||
@@ -54,9 +60,10 @@ public class ReviewService : IReviewService
|
||||
var review = await _db.Reviews.FindAsync(id) ?? throw new NotFoundException("Review", id);
|
||||
if (review.UserId != userId) throw new ForbiddenException();
|
||||
review.Rating = req.Rating; review.Text = req.Text;
|
||||
review.LlmStatus = ReviewLlmStatus.Pending;
|
||||
ResetLlmAnalysis(review);
|
||||
review.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
await _reviewAnalysisQueue.EnqueueAsync(review.Id);
|
||||
return await GetByIdAsync(id);
|
||||
}
|
||||
|
||||
@@ -86,29 +93,34 @@ public class ReviewService : IReviewService
|
||||
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ReviewDto>> GetAllAsync(PaginationRequest pagination)
|
||||
public async Task<PagedResult<ReviewDto>> GetAllAsync(ReviewFilterRequest filter)
|
||||
{
|
||||
var query = BaseQuery();
|
||||
if (filter.LlmStatus.HasValue)
|
||||
query = query.Where(r => r.LlmStatus == filter.LlmStatus.Value);
|
||||
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderByDescending(r => r.CreatedAt)
|
||||
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<ReviewDto>> GetPendingAsync(PaginationRequest pagination)
|
||||
{
|
||||
var query = BaseQuery().Where(r => r.LlmStatus == ReviewLlmStatus.Pending);
|
||||
var total = await query.CountAsync();
|
||||
var items = await query.OrderBy(r => r.CreatedAt)
|
||||
.Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize).ToListAsync();
|
||||
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, pagination.Page, pagination.PageSize);
|
||||
.Skip((filter.Page - 1) * filter.PageSize).Take(filter.PageSize).ToListAsync();
|
||||
return PagedResult<ReviewDto>.Create(items.Select(r => r.ToDto()).ToList(), total, filter.Page, filter.PageSize);
|
||||
}
|
||||
|
||||
public async Task ReanalyzeAsync(int id)
|
||||
{
|
||||
var review = await _db.Reviews.FindAsync(id) ?? throw new NotFoundException("Review", id);
|
||||
review.LlmStatus = ReviewLlmStatus.Pending;
|
||||
ResetLlmAnalysis(review);
|
||||
review.UpdatedAt = DateTime.UtcNow;
|
||||
await _db.SaveChangesAsync();
|
||||
await _reviewAnalysisQueue.EnqueueAsync(review.Id);
|
||||
}
|
||||
|
||||
private static void ResetLlmAnalysis(Review review)
|
||||
{
|
||||
review.LlmStatus = ReviewLlmStatus.Pending;
|
||||
review.Sentiment = null;
|
||||
review.QualityScore = null;
|
||||
review.IsInformative = null;
|
||||
review.LlmTags = null;
|
||||
review.LlmRawOutput = null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user