feat(cv-matcher): add AiPrompts table; remove MyAiDbContext dependency
cv-matcher-data: - Add AiPromptEntity (Key, Language, Value, Description, UpdatedAt) - Add AiPrompts DbSet to CvMatcherDbContext with composite PK - Migration AddAiPrompts: create cvMatcher.AiPrompts table and seed ai.cv-match.system-prompt (language "*") with the current prompt value cv-matcher-api: - Add IAiPromptsRepository / EfAiPromptsRepository under Data/Repositories/ - CvMatcherService: inject IAiPromptsRepository; replace _templates.Render(...) with async DB lookup + simple string replacement - Program.cs: register IAiPromptsRepository (scoped); remove MyAiDbContext, ITemplateService/DbTemplateService registrations and MyAiDbContext migration call - Remove myai-data ProjectReference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
namespace CvMatcher.Data.Repositories.Contracts;
|
||||||
|
|
||||||
|
public interface IAiPromptsRepository
|
||||||
|
{
|
||||||
|
Task<string?> GetAsync(string key, string language, CancellationToken ct);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using CvMatcher.Data;
|
||||||
|
using CvMatcher.Data.Repositories.Contracts;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace CvMatcher.Data.Repositories;
|
||||||
|
|
||||||
|
public sealed class EfAiPromptsRepository : IAiPromptsRepository
|
||||||
|
{
|
||||||
|
private readonly CvMatcherDbContext _db;
|
||||||
|
|
||||||
|
public EfAiPromptsRepository(CvMatcherDbContext db)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetAsync(string key, string language, CancellationToken ct)
|
||||||
|
{
|
||||||
|
return await _db.AiPrompts
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(x => x.Key == key && x.Language == language)
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.FirstOrDefaultAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,8 +10,6 @@ using Api.Services.Contracts;
|
|||||||
using CvMatcher.Models.Settings;
|
using CvMatcher.Models.Settings;
|
||||||
using CvSearch.Data;
|
using CvSearch.Data;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using MyAi.Data;
|
|
||||||
using MyAi.Data.Services;
|
|
||||||
using Refit;
|
using Refit;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Common.Settings;
|
using Common.Settings;
|
||||||
@@ -76,18 +74,8 @@ try
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddDbContext<MyAiDbContext>(options =>
|
|
||||||
{
|
|
||||||
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
|
|
||||||
options.UseSqlServer(connectionString, sql =>
|
|
||||||
{
|
|
||||||
sql.MigrationsAssembly("myai-data");
|
|
||||||
sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
builder.Services.AddSingleton<ITemplateService, DbTemplateService>();
|
|
||||||
|
|
||||||
builder.Services.AddScoped<IMatcherRepository, EfMatcherRepository>();
|
builder.Services.AddScoped<IMatcherRepository, EfMatcherRepository>();
|
||||||
|
builder.Services.AddScoped<IAiPromptsRepository, EfAiPromptsRepository>();
|
||||||
builder.Services.AddScoped<ICvMatcherService, CvMatcherService>();
|
builder.Services.AddScoped<ICvMatcherService, CvMatcherService>();
|
||||||
builder.Services.AddScoped<IJobTokenService, JobTokenService>();
|
builder.Services.AddScoped<IJobTokenService, JobTokenService>();
|
||||||
|
|
||||||
@@ -122,11 +110,6 @@ try
|
|||||||
var db = scope.ServiceProvider.GetRequiredService<CvSearchDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<CvSearchDbContext>();
|
||||||
db.Database.Migrate();
|
db.Database.Migrate();
|
||||||
}
|
}
|
||||||
using (var scope = app.Services.CreateScope())
|
|
||||||
{
|
|
||||||
var db = scope.ServiceProvider.GetRequiredService<MyAiDbContext>();
|
|
||||||
db.Database.Migrate();
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Information("{Service} startup complete", ServiceName);
|
Log.Information("{Service} startup complete", ServiceName);
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using CvMatcher.Models.Responses;
|
|||||||
using CvMatcher.Models.Settings;
|
using CvMatcher.Models.Settings;
|
||||||
using Api.Services.Contracts;
|
using Api.Services.Contracts;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MyAi.Data.Services;
|
|
||||||
|
|
||||||
namespace Api.Services;
|
namespace Api.Services;
|
||||||
|
|
||||||
@@ -17,23 +16,23 @@ public sealed class CvMatcherService : ICvMatcherService
|
|||||||
private readonly IJobTextExtractor _jobTextExtractor;
|
private readonly IJobTextExtractor _jobTextExtractor;
|
||||||
private readonly IMatcherAiClient _ai;
|
private readonly IMatcherAiClient _ai;
|
||||||
private readonly IMatcherRepository _repository;
|
private readonly IMatcherRepository _repository;
|
||||||
|
private readonly IAiPromptsRepository _aiPrompts;
|
||||||
private readonly MatcherSettings _settings;
|
private readonly MatcherSettings _settings;
|
||||||
private readonly ITemplateService _templates;
|
|
||||||
|
|
||||||
public CvMatcherService(
|
public CvMatcherService(
|
||||||
IRagApiClient rag,
|
IRagApiClient rag,
|
||||||
IJobTextExtractor jobTextExtractor,
|
IJobTextExtractor jobTextExtractor,
|
||||||
IMatcherAiClient ai,
|
IMatcherAiClient ai,
|
||||||
IMatcherRepository repository,
|
IMatcherRepository repository,
|
||||||
IOptions<MatcherSettings> options,
|
IAiPromptsRepository aiPrompts,
|
||||||
ITemplateService templates)
|
IOptions<MatcherSettings> options)
|
||||||
{
|
{
|
||||||
_rag = rag;
|
_rag = rag;
|
||||||
_jobTextExtractor = jobTextExtractor;
|
_jobTextExtractor = jobTextExtractor;
|
||||||
_ai = ai;
|
_ai = ai;
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
|
_aiPrompts = aiPrompts;
|
||||||
_settings = options.Value;
|
_settings = options.Value;
|
||||||
_templates = templates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CvUploadResponse> UploadCvAsync(IFormFile file, CancellationToken ct)
|
public async Task<CvUploadResponse> UploadCvAsync(IFormFile file, CancellationToken ct)
|
||||||
@@ -115,8 +114,9 @@ public sealed class CvMatcherService : ICvMatcherService
|
|||||||
var evidence = evidenceChunks.Count > 0 ? string.Join("\n\n", evidenceChunks.Take(4)) : Limit(job.Text, 4000);
|
var evidence = evidenceChunks.Count > 0 ? string.Join("\n\n", evidenceChunks.Take(4)) : Limit(job.Text, 4000);
|
||||||
var languageName = LanguageName(language);
|
var languageName = LanguageName(language);
|
||||||
|
|
||||||
var systemPrompt = _templates.Render("ai.cv-match.system-prompt", "*",
|
var promptTemplate = await _aiPrompts.GetAsync("ai.cv-match.system-prompt", "*", ct)
|
||||||
("languageName", languageName));
|
?? "You are a strict CV-to-job matching engine. Return JSON only.";
|
||||||
|
var systemPrompt = promptTemplate.Replace("{{languageName}}", languageName, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var userPrompt = $"""
|
var userPrompt = $"""
|
||||||
CV:
|
CV:
|
||||||
|
|||||||
@@ -83,6 +83,5 @@
|
|||||||
<ProjectReference Include="..\cv-matcher-data\cv-matcher-data.csproj" />
|
<ProjectReference Include="..\cv-matcher-data\cv-matcher-data.csproj" />
|
||||||
<ProjectReference Include="..\common\common.csproj" />
|
<ProjectReference Include="..\common\common.csproj" />
|
||||||
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
|
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
|
||||||
<ProjectReference Include="..\myai-data\myai-data.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public sealed class CvMatcherDbContext : DbContext
|
|||||||
|
|
||||||
public DbSet<CvMatchResultEntity> CvMatchResults => Set<CvMatchResultEntity>();
|
public DbSet<CvMatchResultEntity> CvMatchResults => Set<CvMatchResultEntity>();
|
||||||
public DbSet<CvMatcherChatCacheEntity> CvMatcherChatCache => Set<CvMatcherChatCacheEntity>();
|
public DbSet<CvMatcherChatCacheEntity> CvMatcherChatCache => Set<CvMatcherChatCacheEntity>();
|
||||||
|
public DbSet<AiPromptEntity> AiPrompts => Set<AiPromptEntity>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -41,5 +42,16 @@ public sealed class CvMatcherDbContext : DbContext
|
|||||||
entity.Property(x => x.ResponseText).IsRequired();
|
entity.Property(x => x.ResponseText).IsRequired();
|
||||||
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<AiPromptEntity>(entity =>
|
||||||
|
{
|
||||||
|
entity.ToTable("AiPrompts");
|
||||||
|
entity.HasKey(x => new { x.Key, x.Language });
|
||||||
|
entity.Property(x => x.Key).HasMaxLength(128);
|
||||||
|
entity.Property(x => x.Language).HasMaxLength(8);
|
||||||
|
entity.Property(x => x.Value).IsRequired();
|
||||||
|
entity.Property(x => x.Description).HasMaxLength(500).HasDefaultValue(string.Empty);
|
||||||
|
entity.Property(x => x.UpdatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace CvMatcher.Data.Entities;
|
||||||
|
|
||||||
|
public sealed class AiPromptEntity
|
||||||
|
{
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
public string Language { get; set; } = string.Empty;
|
||||||
|
public string Value { get; set; } = string.Empty;
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public DateTime UpdatedAt { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using CvMatcher.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CvMatcher.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(CvMatcherDbContext))]
|
||||||
|
[Migration("20260528110000_AddAiPrompts")]
|
||||||
|
partial class AddAiPrompts
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasDefaultSchema("cvMatcher")
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvMatcher.Data.Entities.AiPromptEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)")
|
||||||
|
.HasDefaultValue("");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Key", "Language");
|
||||||
|
|
||||||
|
b.ToTable("AiPrompts", "cvMatcher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("CvDocumentId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("JobDocumentId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<string>("ResultJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<int>("Score")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CvDocumentId", "JobDocumentId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Results", "cvMatcher");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("CacheKey")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("Model")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(120)
|
||||||
|
.HasColumnType("nvarchar(120)");
|
||||||
|
|
||||||
|
b.Property<string>("ResponseText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Temperature")
|
||||||
|
.HasColumnType("decimal(4,2)");
|
||||||
|
|
||||||
|
b.HasKey("CacheKey");
|
||||||
|
|
||||||
|
b.ToTable("ChatCache", "cvMatcher");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace CvMatcher.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAiPrompts : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AiPrompts",
|
||||||
|
schema: "cvMatcher",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Key = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||||
|
Language = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: false),
|
||||||
|
Value = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false, defaultValue: ""),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AiPrompts", x => new { x.Key, x.Language });
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
schema: "cvMatcher",
|
||||||
|
table: "AiPrompts",
|
||||||
|
columns: ["Key", "Language", "Value", "Description"],
|
||||||
|
values: new object[]
|
||||||
|
{
|
||||||
|
"ai.cv-match.system-prompt",
|
||||||
|
"*",
|
||||||
|
"You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100.\nPenalize missing required skills. Do not invent experience. Use concise business language.\nRespond entirely in {{languageName}} — all text fields in the JSON must be in {{languageName}}.\nJSON shape: {\"score\":number,\"summary\":\"...\",\"strengths\":[\"...\"],\"gaps\":[\"...\"],\"recommendations\":[\"...\"],\"evidence\":[\"...\"]}",
|
||||||
|
"System prompt template for the CV-to-job LLM matching call. {{languageName}} is substituted at runtime."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(name: "AiPrompts", schema: "cvMatcher");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,37 @@ namespace CvMatcher.Data.Migrations
|
|||||||
|
|
||||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("CvMatcher.Data.Entities.AiPromptEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Language")
|
||||||
|
.HasMaxLength(8)
|
||||||
|
.HasColumnType("nvarchar(8)");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasMaxLength(500)
|
||||||
|
.HasColumnType("nvarchar(500)")
|
||||||
|
.HasDefaultValue("");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.HasKey("Key", "Language");
|
||||||
|
|
||||||
|
b.ToTable("AiPrompts", "cvMatcher");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
|
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
|
|||||||
Reference in New Issue
Block a user