AI ingestion: dynamic role/category creation + tags, hardcoded read-only prompt
- Unknown roles from the AI are now resolved-or-CREATED (Persian-normalized dedupe) instead of dropped/fallback; new role gets the AI's category, assigned to the applicant. - AI output gains category + tags; AI-detected skills/requirements (ICU, MMT, پروانهدار…) now fold into the applicant's searchable Tags. - System prompt is hardcoded in AppSetting.DefaultPrompt and used directly by the auditor; admin sees it read-only (cannot edit/break it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,10 @@ namespace JobsMedical.Web.Services.Scraping;
|
||||
public record AiStructured(
|
||||
string? Kind, string? Role, string? City, string? District, string? ShiftType,
|
||||
string? EmploymentType, long? PayAmount, int? SharePercent, string? Title, string? FacilityName,
|
||||
string? Phone = null, string? PersonName = null, int? YearsExperience = null, bool? IsLicensed = null);
|
||||
string? Phone = null, string? PersonName = null, int? YearsExperience = null, bool? IsLicensed = null,
|
||||
// Dynamic taxonomy: the model may name a role/category outside the seeded set (ingestion
|
||||
// resolves-or-creates it). Tags carry the post's skills/requirements (ICU, MMT, پروانهدار…).
|
||||
string? Category = null, IReadOnlyList<string>? Tags = null);
|
||||
|
||||
/// <summary>An AI verdict on a raw listing.</summary>
|
||||
public record AiAuditResult(string Decision, int Confidence, string? Reason, AiStructured? Data)
|
||||
@@ -45,7 +48,9 @@ public class OpenAiCompatibleAuditor : IAiAuditor
|
||||
confidence: عدد ۰ تا ۱۰۰
|
||||
reason: توضیح کوتاه فارسی
|
||||
kind: shift (شیفت توسط مرکز) | job (استخدام توسط مرکز) | talent (کادر درمان که خودش «آماده به کار» است)
|
||||
role: عنوان دقیق نقش درمانی (مثل پرستار، پزشک عمومی، دندانپزشک، تکنسین اتاق عمل، ماما، کارشناس آزمایشگاه)
|
||||
role: عنوان دقیق نقش درمانی (مثل پرستار، پزشک عمومی، دندانپزشک، تکنسین اتاق عمل، ماما، کارشناس آزمایشگاه). اگر تخصص دقیق در فهرست نبود، همان عنوان دقیق را برگردان.
|
||||
category: گروه نقش (پزشک | پرستار | ماما | تکنسین | دندانپزشک). اگر هیچکدام مناسب نبود، یک گروه کوتاه و مناسب پیشنهاد بده.
|
||||
tags: آرایهای از کلیدواژههای مهارت/الزام مرتبط بهصورت رشته (مثل "ICU"، "MMT"، "CPR"، "پروانهدار"، "خانم") یا []
|
||||
city, district: نام شهر و محله/منطقه در صورت ذکر
|
||||
shiftType: day|evening|night|oncall (فقط برای shift)
|
||||
employmentType: fulltime|parttime|contract|plan
|
||||
@@ -150,9 +155,9 @@ public class OpenAiCompatibleAuditor : IAiAuditor
|
||||
response_format = new { type = "json_object" },
|
||||
messages = new object[]
|
||||
{
|
||||
// Admin prompt + an authoritative output schema, so classification/tags stay
|
||||
// correct even if the stored prompt predates the talent/phone fields.
|
||||
new { role = "system", content = s.AiSystemPrompt + "\n\n" + OutputSchema },
|
||||
// Hardcoded, code-owned prompt (NOT the stored AiSystemPrompt) + the authoritative
|
||||
// output schema, so classification/tags can never be broken by an admin edit.
|
||||
new { role = "system", content = AppSetting.DefaultPrompt + "\n\n" + OutputSchema },
|
||||
new { role = "user", content = "آگهی خام:\n" + rawText + "\n\nفقط با JSON پاسخ بده." },
|
||||
},
|
||||
};
|
||||
@@ -214,11 +219,23 @@ public class OpenAiCompatibleAuditor : IAiAuditor
|
||||
long? L(string k) => r.TryGetProperty(k, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt64(out var n) ? n : null;
|
||||
int? NI(string k) => r.TryGetProperty(k, out var v) && v.ValueKind == JsonValueKind.Number && v.TryGetInt32(out var n) ? n : null;
|
||||
bool? B(string k) => r.TryGetProperty(k, out var v) && (v.ValueKind == JsonValueKind.True || v.ValueKind == JsonValueKind.False) ? v.GetBoolean() : null;
|
||||
// Array-of-strings reader (tolerates the model returning a single string instead of an array).
|
||||
IReadOnlyList<string>? SA(string k)
|
||||
{
|
||||
if (!r.TryGetProperty(k, out var v)) return null;
|
||||
var list = new List<string>();
|
||||
if (v.ValueKind == JsonValueKind.Array)
|
||||
foreach (var el in v.EnumerateArray())
|
||||
if (el.ValueKind == JsonValueKind.String && el.GetString() is { Length: > 0 } s) list.Add(s);
|
||||
else if (v.ValueKind == JsonValueKind.String && v.GetString() is { Length: > 0 } one) list.Add(one);
|
||||
return list.Count > 0 ? list : null;
|
||||
}
|
||||
|
||||
var decision = (S("decision") ?? "review").ToLowerInvariant();
|
||||
var data = new AiStructured(S("kind"), S("role"), S("city"), S("district"), S("shiftType"),
|
||||
S("employmentType"), L("payAmount"), NI("sharePercent"), S("title"), S("facilityName"),
|
||||
Phone: S("phone"), PersonName: S("personName"), YearsExperience: NI("yearsExperience"), IsLicensed: B("isLicensed"));
|
||||
Phone: S("phone"), PersonName: S("personName"), YearsExperience: NI("yearsExperience"), IsLicensed: B("isLicensed"),
|
||||
Category: S("category"), Tags: SA("tags"));
|
||||
return new AiAuditResult(decision, Math.Clamp(I("confidence", 50), 0, 100), S("reason"), data);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user