diff --git a/Apis/cv-matcher-data/Migrations/20260609133623_FixKeywordExtractionPrompt.Designer.cs b/Apis/cv-matcher-data/Migrations/20260609133623_FixKeywordExtractionPrompt.Designer.cs
new file mode 100644
index 0000000..3b15170
--- /dev/null
+++ b/Apis/cv-matcher-data/Migrations/20260609133623_FixKeywordExtractionPrompt.Designer.cs
@@ -0,0 +1,138 @@
+//
+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
+ {
+ ///
+ 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("Key")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Language")
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)");
+
+ b.Property("Description")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)")
+ .HasDefaultValue("");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Key", "Language");
+
+ b.ToTable("AiPrompts", "cvMatcher");
+ });
+
+ modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ClientIpAddress")
+ .HasMaxLength(45)
+ .HasColumnType("nvarchar(45)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CvDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("JobDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Language")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ResultJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("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("CacheKey")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("Model")
+ .IsRequired()
+ .HasMaxLength(120)
+ .HasColumnType("nvarchar(120)");
+
+ b.Property("ResponseText")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Temperature")
+ .HasColumnType("decimal(4,2)");
+
+ b.HasKey("CacheKey");
+
+ b.ToTable("ChatCache", "cvMatcher");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Apis/cv-matcher-data/Migrations/20260609133623_FixKeywordExtractionPrompt.cs b/Apis/cv-matcher-data/Migrations/20260609133623_FixKeywordExtractionPrompt.cs
new file mode 100644
index 0000000..4c32116
--- /dev/null
+++ b/Apis/cv-matcher-data/Migrations/20260609133623_FixKeywordExtractionPrompt.cs
@@ -0,0 +1,93 @@
+using CvMatcher.Data;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace CvMatcher.Data.Migrations
+{
+ ///
+ 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.";
+
+ ///
+ 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."
+ ]);
+ }
+
+ ///
+ 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."
+ ]);
+ }
+ }
+}