Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb83d28ed5 | |||
| b6d9aea3bc | |||
| 2b9132a3a9 | |||
| e5bf56cc4d | |||
| 8f58708cd9 | |||
| 06dd0140d6 | |||
| 0aee7c4ed6 | |||
| cd661fe613 |
@@ -3,7 +3,7 @@ name: Build and Push Docker Images Staging
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- staging
|
||||
- production
|
||||
|
||||
env:
|
||||
GIT_HOST: git.easysoft.ro
|
||||
@@ -16,7 +16,7 @@ env:
|
||||
CV_CLEANUP_JOB_IMAGE: apps/myai-cv-cleanup-job
|
||||
CV_SEARCH_JOB_IMAGE: apps/myai-cv-search-job
|
||||
PAGE_FETCHER_API_IMAGE: apps/myai-page-fetcher-api
|
||||
IMAGE_TAG: staging
|
||||
IMAGE_TAG: production
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -97,10 +97,4 @@ jobs:
|
||||
|
||||
- name: Push Page Fetcher API image
|
||||
run: |
|
||||
docker push "${REGISTRY_HOST}/${PAGE_FETCHER_API_IMAGE}:${IMAGE_TAG}"
|
||||
|
||||
- name: Reclaim disk space
|
||||
if: always()
|
||||
run: |
|
||||
docker image prune -af
|
||||
docker builder prune -af
|
||||
docker push "${REGISTRY_HOST}/${PAGE_FETCHER_API_IMAGE}:${IMAGE_TAG}"
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
// <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("20260609133623_FixKeywordExtractionPrompt")]
|
||||
partial class FixKeywordExtractionPrompt
|
||||
{
|
||||
/// <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<string>("ClientIpAddress")
|
||||
.HasMaxLength(45)
|
||||
.HasColumnType("nvarchar(45)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("datetime2")
|
||||
.HasDefaultValueSql("SYSUTCDATETIME()");
|
||||
|
||||
b.Property<string>("CvDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("nvarchar(256)");
|
||||
|
||||
b.Property<string>("JobDocumentId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Language")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(450)");
|
||||
|
||||
b.Property<string>("ResultJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("Score")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CvDocumentId", "JobDocumentId", "Language")
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
using CvMatcher.Data;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CvMatcher.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class FixKeywordExtractionPrompt : Migration
|
||||
{
|
||||
// Full prompt values — only the 'keywords' instruction changes vs. the previous migration.
|
||||
// Stored in full so Down() can restore the previous version exactly.
|
||||
|
||||
private const string EnNew =
|
||||
"You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100. Penalize missing required skills. Do not invent experience. Use concise business language. All text fields in the JSON response must be in English.\n" +
|
||||
"JSON shape: {\"score\":number,\"summary\":\"one-line summary in English\",\"strengths\":[\"strength 1 in English\"],\"gaps\":[\"gap 1 in English\"],\"recommendations\":[\"recommendation 1 in English\"],\"evidence\":[\"evidence 1 in English\"],\"keywords\":[\"Senior .NET Developer\",\"C#\",\"Azure\"],\"location\":\"City, Country\"}.\n" +
|
||||
"For 'keywords': extract 2-4 job-board search terms that represent the candidate's professional identity as shown in their CV — their seniority level and primary role title (e.g. 'Software Architect', 'Engineering Manager', 'Senior .NET Developer') plus 1-2 core technologies they genuinely emphasize throughout the CV. Derive these entirely from the CV — do not use the job title or job technologies unless they independently match the candidate's actual positioning. Avoid generic terms like 'developer', 'engineer', 'cloud', or 'leadership'.\n" +
|
||||
"For 'location': extract the candidate's city and country from the CV (e.g. 'Cluj-Napoca, Romania'). Use an empty string if not found.";
|
||||
|
||||
private const string EnPrev =
|
||||
"You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100. Penalize missing required skills. Do not invent experience. Use concise business language. All text fields in the JSON response must be in English.\n" +
|
||||
"JSON shape: {\"score\":number,\"summary\":\"one-line summary in English\",\"strengths\":[\"strength 1 in English\"],\"gaps\":[\"gap 1 in English\"],\"recommendations\":[\"recommendation 1 in English\"],\"evidence\":[\"evidence 1 in English\"],\"keywords\":[\"Senior .NET Developer\",\"C#\",\"Azure\"],\"location\":\"City, Country\"}.\n" +
|
||||
"For 'keywords': extract 2-4 short, concrete terms a recruiter would search for on a job board — the candidate's primary role title and key technologies (e.g. 'Senior .NET Developer', 'C#', 'Azure'). Avoid abstract concepts like 'leadership', 'cloud', or 'microservices'.\n" +
|
||||
"For 'location': extract the candidate's city and country from the CV (e.g. 'Cluj-Napoca, Romania'). Use an empty string if not found.";
|
||||
|
||||
private const string RoNew =
|
||||
"Ești un motor strict de potrivire CV-job. Returnează doar JSON. Punctează realist între 0 și 100. Penalizează abilitățile lipsă necesare. Nu inventa experiență. Folosește limbaj profesional concis. Toate câmpurile text din răspunsul JSON trebuie să fie în limba română.\n" +
|
||||
"JSON shape: {\"score\":number,\"summary\":\"rezumat pe o linie în română\",\"strengths\":[\"punct forte 1 în română\"],\"gaps\":[\"lipsă 1 în română\"],\"recommendations\":[\"recomandare 1 în română\"],\"evidence\":[\"dovadă 1 în română\"],\"keywords\":[\"Senior .NET Developer\",\"C#\",\"Azure\"],\"location\":\"Oraș, Țară\"}.\n" +
|
||||
"Pentru 'keywords': extrage 2-4 termeni de căutare pe site-uri de joburi care reprezintă identitatea profesională a candidatului conform CV-ului — nivelul de senioritate și titlul principal de rol (ex. 'Software Architect', 'Engineering Manager', 'Senior .NET Developer') și 1-2 tehnologii de bază pe care candidatul le evidențiază cu adevărat în CV. Derivă aceștia exclusiv din CV — nu folosi titlul jobului sau tehnologiile din job dacă nu corespund poziționării reale a candidatului. Evită termeni generici precum 'developer', 'engineer', 'cloud' sau 'leadership'.\n" +
|
||||
"Pentru 'location': extrage orașul și țara candidatului din CV (ex. 'Cluj-Napoca, România'). Folosește string gol dacă nu se găsește.";
|
||||
|
||||
private const string RoPrev =
|
||||
"Ești un motor strict de potrivire CV-job. Returnează doar JSON. Punctează realist între 0 și 100. Penalizează abilitățile lipsă necesare. Nu inventa experiență. Folosește limbaj profesional concis. Toate câmpurile text din răspunsul JSON trebuie să fie în limba română.\n" +
|
||||
"JSON shape: {\"score\":number,\"summary\":\"rezumat pe o linie în română\",\"strengths\":[\"punct forte 1 în română\"],\"gaps\":[\"lipsă 1 în română\"],\"recommendations\":[\"recomandare 1 în română\"],\"evidence\":[\"dovadă 1 în română\"],\"keywords\":[\"Senior .NET Developer\",\"C#\",\"Azure\"],\"location\":\"Oraș, Țară\"}.\n" +
|
||||
"Pentru 'keywords': extrage 2-4 termeni scurți și concreți pe care un recrutor i-ar căuta pe un site de joburi — titlul principal al rolului și tehnologiile cheie (ex. 'Senior .NET Developer', 'C#', 'Azure'). Evită concepte abstracte precum 'leadership', 'cloud' sau 'microservicii'.\n" +
|
||||
"Pentru 'location': extrage orașul și țara candidatului din CV (ex. 'Cluj-Napoca, România'). Folosește string gol dacă nu se găsește.";
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Update English prompt: keywords must now be derived from the CV only,
|
||||
// not influenced by the job description being matched against.
|
||||
migrationBuilder.UpdateData(
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "AiPrompts",
|
||||
keyColumns: ["Key", "Language"],
|
||||
keyValues: ["ai.cv-match.system-prompt", "en"],
|
||||
columns: ["Value", "Description"],
|
||||
values: [
|
||||
EnNew,
|
||||
"System prompt for CV-to-job matching in English. Keywords represent the candidate's CV identity (seniority + role + core tech), not the job being matched."
|
||||
]);
|
||||
|
||||
// Update Romanian prompt: same improvement.
|
||||
migrationBuilder.UpdateData(
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "AiPrompts",
|
||||
keyColumns: ["Key", "Language"],
|
||||
keyValues: ["ai.cv-match.system-prompt", "ro"],
|
||||
columns: ["Value", "Description"],
|
||||
values: [
|
||||
RoNew,
|
||||
"System prompt pentru potrivire CV-job în română. Cuvintele cheie reprezintă identitatea CV-ului candidatului (senioritate + rol + tehnologii cheie), nu jobul cu care se face potrivirea."
|
||||
]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.UpdateData(
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "AiPrompts",
|
||||
keyColumns: ["Key", "Language"],
|
||||
keyValues: ["ai.cv-match.system-prompt", "en"],
|
||||
columns: ["Value", "Description"],
|
||||
values: [
|
||||
EnPrev,
|
||||
"System prompt for CV-to-job matching in English. Extracts job-board-friendly keywords (role title + key tech) and candidate location."
|
||||
]);
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
schema: MigrationConstants.SchemaName,
|
||||
table: "AiPrompts",
|
||||
keyColumns: ["Key", "Language"],
|
||||
keyValues: ["ai.cv-match.system-prompt", "ro"],
|
||||
columns: ["Value", "Description"],
|
||||
values: [
|
||||
RoPrev,
|
||||
"System prompt pentru potrivire CV-job în limba română. Extrage cuvinte cheie prietenoase pentru site-uri de joburi (titlu rol + tehnologii cheie) și locația candidatului."
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Email" Version="4.2.1" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<!-- Swagger -->
|
||||
|
||||
@@ -39,10 +39,11 @@ public static class StartupExtensions
|
||||
.ReadFrom.Configuration(context.Configuration)
|
||||
.ReadFrom.Services(services)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Application", serviceName)
|
||||
.Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
|
||||
.Enrich.WithMachineName()
|
||||
.Enrich.WithEnvironmentName()
|
||||
.Enrich.WithProperty("Service", serviceName)
|
||||
.Enrich.WithProperty("AppVersion", appVersion)
|
||||
.WriteTo.Console(new Serilog.Formatting.Compact.CompactJsonFormatter());
|
||||
.WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter());
|
||||
|
||||
AddEmailSinkIfConfigured(configuration, context.Configuration, serviceName);
|
||||
});
|
||||
@@ -56,10 +57,11 @@ public static class StartupExtensions
|
||||
.ReadFrom.Configuration(builder.Configuration)
|
||||
.ReadFrom.Services(services)
|
||||
.Enrich.FromLogContext()
|
||||
.Enrich.WithProperty("Application", serviceName)
|
||||
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
|
||||
.Enrich.WithMachineName()
|
||||
.Enrich.WithEnvironmentName()
|
||||
.Enrich.WithProperty("Service", serviceName)
|
||||
.Enrich.WithProperty("AppVersion", appVersion)
|
||||
.WriteTo.Console(new Serilog.Formatting.Compact.CompactJsonFormatter());
|
||||
.WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter());
|
||||
|
||||
AddEmailSinkIfConfigured(configuration, builder.Configuration, serviceName);
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<PackageReference Include="DotNetEnv" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" />
|
||||
<PackageReference Include="Serilog.Sinks.Email" />
|
||||
<PackageReference Include="Serilog.Sinks.File" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
|
||||
Reference in New Issue
Block a user