NAMEGEN-2-BE: 5 neue Themen-Kategorien (disney/pokemon/encities/hrcities/usstates)
UsageMap bildet alle Codes auf deutsche Beschreibungen ab; thematische Kategorien landen unter "Themen-Kategorien" im Prompt (kein Etymologie-Zwang, origin=Kategoriename). 22 neue Tests, 179/179 gruen.
This commit is contained in:
@@ -8,8 +8,8 @@ using Microsoft.Extensions.Options;
|
|||||||
namespace GerbilManager.Tests
|
namespace GerbilManager.Tests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// FEAT-NAMEGEN: NameSuggestionService — prompt assembly, JSON parse (incl. Markdown
|
/// FEAT-NAMEGEN / NAMEGEN-2-BE: NameSuggestionService — prompt assembly, usage-code mapping,
|
||||||
/// fence strip), 503-not-configured path, upstream-error path.
|
/// JSON parse (incl. Markdown fence strip), 503-not-configured path, upstream-error path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class NameSuggestionTests
|
public class NameSuggestionTests
|
||||||
{
|
{
|
||||||
@@ -26,6 +26,14 @@ namespace GerbilManager.Tests
|
|||||||
Assert.Contains("origin", prompt);
|
Assert.Contains("origin", prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SystemPrompt_erklärt_thematische_Kategorien()
|
||||||
|
{
|
||||||
|
var prompt = NameSuggestionService.BuildSystemPrompt();
|
||||||
|
Assert.Contains("thematischen Kategorien", prompt);
|
||||||
|
Assert.Contains("Erfinde KEINE Etymologie", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UserPrompt_enthält_Anzahl_und_Anfangsbuchstaben()
|
public void UserPrompt_enthält_Anzahl_und_Anfangsbuchstaben()
|
||||||
{
|
{
|
||||||
@@ -33,7 +41,10 @@ namespace GerbilManager.Tests
|
|||||||
Assert.Contains("6", prompt);
|
Assert.Contains("6", prompt);
|
||||||
Assert.Contains("\"A\"", prompt);
|
Assert.Contains("\"A\"", prompt);
|
||||||
Assert.Contains("weibliche", prompt);
|
Assert.Contains("weibliche", prompt);
|
||||||
Assert.Contains("norn,mythg", prompt);
|
// codes are mapped to German descriptions
|
||||||
|
Assert.Contains("Nordische/Altnordische Etymologie", prompt);
|
||||||
|
Assert.Contains("Griechische Mythologie", prompt);
|
||||||
|
Assert.Contains("Kulturkreisen", prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -54,6 +65,67 @@ namespace GerbilManager.Tests
|
|||||||
Assert.DoesNotContain("männliche", prompt);
|
Assert.DoesNotContain("männliche", prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── NAMEGEN-2-BE: neue Themen-Kategorien ─────────────────────────────
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("disney", "Disney-Charaktere")]
|
||||||
|
[InlineData("pokemon", "Pokémon-Namen")]
|
||||||
|
[InlineData("encities", "Namen englischer Städte")]
|
||||||
|
[InlineData("hrcities", "Namen kroatischer Städte")]
|
||||||
|
[InlineData("usstates", "Namen von US-Bundesstaaten")]
|
||||||
|
public void UsageMap_enthält_alle_fünf_neuen_Codes(string code, string expectedDescription)
|
||||||
|
{
|
||||||
|
Assert.True(NameSuggestionService.UsageMap.TryGetValue(code, out var entry));
|
||||||
|
Assert.Equal(expectedDescription, entry.Description);
|
||||||
|
Assert.True(entry.Thematic);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("disney", "Disney-Charaktere")]
|
||||||
|
[InlineData("pokemon", "Pokémon-Namen")]
|
||||||
|
[InlineData("encities", "Namen englischer Städte")]
|
||||||
|
[InlineData("hrcities", "Namen kroatischer Städte")]
|
||||||
|
[InlineData("usstates", "Namen von US-Bundesstaaten")]
|
||||||
|
public void UserPrompt_enthält_Themen_Kategorie_Beschreibung(string code, string expectedDescription)
|
||||||
|
{
|
||||||
|
var prompt = NameSuggestionService.BuildUserPrompt(null, null, code, 5);
|
||||||
|
Assert.Contains(expectedDescription, prompt);
|
||||||
|
Assert.Contains("Themen-Kategorien", prompt);
|
||||||
|
Assert.DoesNotContain("Kulturkreisen", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UserPrompt_trennt_etym_und_thematische_Kategorien()
|
||||||
|
{
|
||||||
|
var prompt = NameSuggestionService.BuildUserPrompt("D", null, "norn,disney", 4);
|
||||||
|
Assert.Contains("Nordische/Altnordische Etymologie", prompt);
|
||||||
|
Assert.Contains("Kulturkreisen", prompt);
|
||||||
|
Assert.Contains("Disney-Charaktere", prompt);
|
||||||
|
Assert.Contains("Themen-Kategorien", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UserPrompt_thematisch_enthält_Geschlecht_Hinweis()
|
||||||
|
{
|
||||||
|
var prompt = NameSuggestionService.BuildUserPrompt(null, "female", "encities", 3);
|
||||||
|
Assert.Contains("Geschlecht-Filter kann ignoriert werden", prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SuggestAsync_parst_thematische_Kategorie_Antwort()
|
||||||
|
{
|
||||||
|
var payload = """[{"name":"Dumbo","meaning":"Elefant mit großen Ohren aus dem Disney-Film Dumbo (1941)","origin":"Disney"}]""";
|
||||||
|
var handler = new StubHandler(_ => Canned(payload));
|
||||||
|
var service = CreateService("https://api.example.com/v1", "k", "m", handler);
|
||||||
|
|
||||||
|
var result = await service.SuggestAsync("D", null, "disney", 1);
|
||||||
|
|
||||||
|
Assert.Equal(NameSuggestionStatus.Ok, result.Status);
|
||||||
|
Assert.NotNull(result.Suggestions);
|
||||||
|
Assert.Equal("Dumbo", result.Suggestions![0].Name);
|
||||||
|
Assert.Equal("Disney", result.Suggestions[0].Origin);
|
||||||
|
}
|
||||||
|
|
||||||
// ── JSON parsing ──────────────────────────────────────────────────────
|
// ── JSON parsing ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -20,6 +20,23 @@ namespace GerbilManagerWebAPI.Names
|
|||||||
PropertyNameCaseInsensitive = true,
|
PropertyNameCaseInsensitive = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Known usage codes → (German description, isThematic).
|
||||||
|
// Thematic categories provide real source names (not etymology); unknown codes fall through as etym.
|
||||||
|
internal static readonly Dictionary<string, (string Description, bool Thematic)> UsageMap =
|
||||||
|
new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["norn"] = ("Nordische/Altnordische Etymologie", false),
|
||||||
|
["japa"] = ("Japanische Etymologie", false),
|
||||||
|
["mythg"] = ("Griechische Mythologie", false),
|
||||||
|
["ger"] = ("Germanische/Deutsche Etymologie", false),
|
||||||
|
["arb"] = ("Arabische Etymologie", false),
|
||||||
|
["disney"] = ("Disney-Charaktere", true),
|
||||||
|
["pokemon"] = ("Pokémon-Namen", true),
|
||||||
|
["encities"] = ("Namen englischer Städte", true),
|
||||||
|
["hrcities"] = ("Namen kroatischer Städte", true),
|
||||||
|
["usstates"] = ("Namen von US-Bundesstaaten", true),
|
||||||
|
};
|
||||||
|
|
||||||
public async Task<NameSuggestionResult> SuggestAsync(
|
public async Task<NameSuggestionResult> SuggestAsync(
|
||||||
string? letter, string? gender, string? usages, int count,
|
string? letter, string? gender, string? usages, int count,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
@@ -44,7 +61,11 @@ namespace GerbilManagerWebAPI.Names
|
|||||||
"Du bist ein Helfer für Rennmaus-Züchter. " +
|
"Du bist ein Helfer für Rennmaus-Züchter. " +
|
||||||
"Antworte IMMER mit einem reinen JSON-Array — KEINE Markdown-Code-Blöcke, " +
|
"Antworte IMMER mit einem reinen JSON-Array — KEINE Markdown-Code-Blöcke, " +
|
||||||
"KEINE Erklärungen, KEIN Text außerhalb des Arrays. " +
|
"KEINE Erklärungen, KEIN Text außerhalb des Arrays. " +
|
||||||
"Jedes Element hat genau die Felder: name, meaning, origin (alle Strings, alle auf Deutsch).";
|
"Jedes Element hat genau die Felder: name, meaning, origin (alle Strings, alle auf Deutsch). " +
|
||||||
|
"Bei thematischen Kategorien (Disney, Pokémon, Städte, Bundesstaaten): " +
|
||||||
|
"origin = Kategoriename (z.B. \"Disney\", \"Pokémon\", \"Englische Stadt\", \"Kroatische Stadt\", \"US-Bundesstaat\"), " +
|
||||||
|
"meaning = kurzer Kontext aus der Quelle (z.B. Film/Figur-Beschreibung, Lage der Stadt, Bundesstaat-Bezug). " +
|
||||||
|
"Erfinde KEINE Etymologie für thematische Kategorien.";
|
||||||
|
|
||||||
internal static string BuildUserPrompt(string? letter, string? gender, string? usages, int count)
|
internal static string BuildUserPrompt(string? letter, string? gender, string? usages, int count)
|
||||||
{
|
{
|
||||||
@@ -55,10 +76,44 @@ namespace GerbilManagerWebAPI.Names
|
|||||||
if (!string.IsNullOrWhiteSpace(gender) &&
|
if (!string.IsNullOrWhiteSpace(gender) &&
|
||||||
!gender.Equals("any", StringComparison.OrdinalIgnoreCase))
|
!gender.Equals("any", StringComparison.OrdinalIgnoreCase))
|
||||||
sb.Append($", passend für {(gender.Equals("female", StringComparison.OrdinalIgnoreCase) ? "weibliche" : "männliche")} Tiere");
|
sb.Append($", passend für {(gender.Equals("female", StringComparison.OrdinalIgnoreCase) ? "weibliche" : "männliche")} Tiere");
|
||||||
|
|
||||||
|
bool hasThematic = false;
|
||||||
if (!string.IsNullOrWhiteSpace(usages))
|
if (!string.IsNullOrWhiteSpace(usages))
|
||||||
sb.Append($", aus den Kulturkreisen: {usages}");
|
{
|
||||||
sb.Append(". Jeder Name muss eine echte etymologische Bedeutung und Herkunft haben ");
|
var codes = usages.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
sb.Append("(keine erfundenen oder zufälligen Namen). ");
|
var etymDescriptions = new List<string>();
|
||||||
|
var thematicDescriptions = new List<string>();
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
if (UsageMap.TryGetValue(code, out var entry))
|
||||||
|
{
|
||||||
|
if (entry.Thematic) thematicDescriptions.Add(entry.Description);
|
||||||
|
else etymDescriptions.Add(entry.Description);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
etymDescriptions.Add(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (etymDescriptions.Count > 0)
|
||||||
|
sb.Append($", aus den Kulturkreisen: {string.Join(", ", etymDescriptions)}");
|
||||||
|
if (thematicDescriptions.Count > 0)
|
||||||
|
sb.Append($", aus den Themen-Kategorien: {string.Join(", ", thematicDescriptions)}");
|
||||||
|
hasThematic = thematicDescriptions.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(". ");
|
||||||
|
if (hasThematic)
|
||||||
|
{
|
||||||
|
sb.Append("Für Kulturkreis-Namen: echte etymologische Bedeutung und Herkunft. ");
|
||||||
|
sb.Append("Für Themen-Kategorien: echte Namen aus der Quelle, origin = Kategoriename, ");
|
||||||
|
sb.Append("Geschlecht-Filter kann ignoriert werden wenn nicht sinnvoll anwendbar. ");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append("Jeder Name muss eine echte etymologische Bedeutung und Herkunft haben ");
|
||||||
|
sb.Append("(keine erfundenen oder zufälligen Namen). ");
|
||||||
|
}
|
||||||
sb.Append($"Antworte mit genau {count} Elementen als reines JSON-Array: ");
|
sb.Append($"Antworte mit genau {count} Elementen als reines JSON-Array: ");
|
||||||
sb.Append("[{\"name\":\"...\",\"meaning\":\"...\",\"origin\":\"...\"}]");
|
sb.Append("[{\"name\":\"...\",\"meaning\":\"...\",\"origin\":\"...\"}]");
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
|
|||||||
Reference in New Issue
Block a user