From 4187761a41010634545d61af0aca5ec9054b5e51 Mon Sep 17 00:00:00 2001 From: NICK BRAIN Date: Sat, 7 Dec 2024 18:52:13 +0200 Subject: [PATCH 1/3] + GetSampleSentence json input --- My1kWordsEe/Services/Ai/OpenAiClient.cs | 30 +---------- .../Services/Cqs/AddSampleSentenceCommand.cs | 52 +++++++++++++++++-- .../Services/Cqs/ValidateSampleWordCommand.cs | 1 + 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/My1kWordsEe/Services/Ai/OpenAiClient.cs b/My1kWordsEe/Services/Ai/OpenAiClient.cs index ab5e11a..ed4f5f2 100644 --- a/My1kWordsEe/Services/Ai/OpenAiClient.cs +++ b/My1kWordsEe/Services/Ai/OpenAiClient.cs @@ -49,11 +49,12 @@ public async Task> CompleteAsync(string instructions, string inpu } } - public async Task> CompleteJsonAsync(string instructions, string input) + public async Task> CompleteJsonAsync(string instructions, string input, float? temperature = null) { var response = await this.CompleteAsync(instructions, input, new ChatCompletionOptions { ResponseFormat = ChatResponseFormat.JsonObject, + Temperature = temperature }); if (response.IsFailure) @@ -171,26 +172,6 @@ public static async Task> GetWordMetadata( }); } - public static async Task> GetSampleSentence(this OpenAiClient openAiClient, string eeWord, string explanation, string[]? existingSamples = null) - { - var prompt = - "Sa oled keeleõppe süsteemi abiline, mis aitab õppida enim levinud eesti keele sõnu.\n" + - "Sinu sisend on üks eestikeelne sõna ja selle rakenduse kontekst: ().\n" + - "Sinu ülesanne on kirjutada selle kasutamise kohta lihtne lühike näitelause, kasutades seda sõna.\n" + - "Lauses kasuta kõige levinuimaid ja lihtsamaid sõnu eesti keeles et toetada keeleõpet.\n" + - "Eelistan SVO-lausete sõnajärge, kus esikohal on subjekt (S), seejärel tegusõna (V) ja objekt (O)\n" + - "Lausel peaks olema praktiline tegelik elu mõte\n" + - "Teie väljundiks on JSON-objekt koos eestikeelse näidislausega ja sellele vastav tõlge inglise keelde vastavalt lepingule:\n" + - "```\n{\n" + - "\"ee_sentence\": \"\", \"en_sentence\": \"\"" + - "\n}\n```\n" + - ((existingSamples != null && existingSamples.Any()) - ? "PS: Ärge korrake järgmisi näidiseid, olge erinevad:\n" + string.Join(",", existingSamples.Select(s => $"'{s}'")) - : string.Empty); - - return await openAiClient.CompleteJsonAsync(prompt, $"{eeWord} (${explanation})"); - } - private class WordMetadata { [JsonPropertyName("ee_word")] @@ -210,12 +191,5 @@ private class WordMetadata } } - public class Sentence - { - [JsonPropertyName("ee_sentence")] - public required string Ee { get; set; } - [JsonPropertyName("en_sentence")] - public required string En { get; set; } - } } diff --git a/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs b/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs index eb40207..c5085a0 100644 --- a/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs +++ b/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs @@ -1,3 +1,6 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + using CSharpFunctionalExtensions; using My1kWordsEe.Models; @@ -33,10 +36,8 @@ public async Task> Invoke(SampleWord word) return Result.Failure($"Too many samples. {MaxSamples} is a maximum"); } - var sentence = await this.openAiService.GetSampleSentence( - eeWord: word.EeWord, - explanation: word.EeExplanation ?? word.EnExplanation, - existingSamples: word.Samples.Select(s => s.EeSentence).ToArray()); + var sentence = await this.GetSampleSentence(word); + if (sentence.IsFailure) { return Result.Failure($"Sentence generation failed: {sentence.Error}"); @@ -80,5 +81,48 @@ private Task> GenerateImage(Sentence sentence) => private Task> GenerateSpeech(Sentence sentence) => this.addAudioCommand.Invoke(sentence.Ee); + + private async Task> GetSampleSentence(SampleWord word) + { + var prompt = + "Sa oled keeleõppe süsteemi abiline, mis aitab õppida enim levinud eesti keele sõnu.\n" + + + "Teie sisend on JSON-objekt:" + + "```\n{\n" + + "\"EeWord\": \"\", " + + "\"EnWord\": \"\n" + + "\"EnExplanation\": \"\n" + + "}\n```\n" + + + "Sinu sisend on üks eestikeelne sõna ja selle rakenduse kontekst: ().\n" + + "Sinu ülesanne on kirjutada selle kasutamise kohta lihtne lühike näitelause, kasutades seda sõna.\n" + + "Lauses kasuta kõige levinuimaid ja lihtsamaid sõnu eesti keeles et toetada keeleõpet.\n" + + "Eelistan SVO-lausete sõnajärge, kus esikohal on subjekt (S), seejärel tegusõna (V) ja objekt (O)\n" + + "Lausel peaks olema praktiline tegelik elu mõte\n" + + "Teie väljundiks on JSON-objekt koos eestikeelse näidislausega ja sellele vastav tõlge inglise keelde vastavalt lepingule:\n" + + "```\n{\n" + + "\"ee_sentence\": \"\", \"en_sentence\": \"\"" + + "\n}\n```\n"; + + var input = JsonSerializer.Serialize(new + { + word.EeWord, + word.EnWord, + word.EnExplanation + }); + + var result = await this.openAiService.CompleteJsonAsync(prompt, input, temperature: 0.7f); + + return result; + } + + private class Sentence + { + [JsonPropertyName("ee_sentence")] + public required string Ee { get; set; } + + [JsonPropertyName("en_sentence")] + public required string En { get; set; } + } } } \ No newline at end of file diff --git a/My1kWordsEe/Services/Cqs/ValidateSampleWordCommand.cs b/My1kWordsEe/Services/Cqs/ValidateSampleWordCommand.cs index 2c03354..ced3c4a 100644 --- a/My1kWordsEe/Services/Cqs/ValidateSampleWordCommand.cs +++ b/My1kWordsEe/Services/Cqs/ValidateSampleWordCommand.cs @@ -26,6 +26,7 @@ public async Task> Invoke(SampleWord sample) "\"EnWord\": \"\n" + "\"EnExplanation\": \"\n" + "}\n```\n" + + "Your outpur is JSON object:\n" + "```\n{\n" + "\"IsValid\": ,\n" + From 2db9159258148e4f2a13a7a7f35e6d4a9d48e4d9 Mon Sep 17 00:00:00 2001 From: NICK BRAIN Date: Sat, 7 Dec 2024 19:10:01 +0200 Subject: [PATCH 2/3] + GetWordMetadata json input --- My1kWordsEe/Services/Ai/OpenAiClient.cs | 83 -------------- .../Services/Cqs/AddSampleSentenceCommand.cs | 22 ++-- .../Services/Cqs/AddSampleWordCommand.cs | 107 ++++++++++++++++-- .../Services/Cqs/ValidateSampleWordCommand.cs | 16 +-- 4 files changed, 120 insertions(+), 108 deletions(-) diff --git a/My1kWordsEe/Services/Ai/OpenAiClient.cs b/My1kWordsEe/Services/Ai/OpenAiClient.cs index ed4f5f2..1b8872e 100644 --- a/My1kWordsEe/Services/Ai/OpenAiClient.cs +++ b/My1kWordsEe/Services/Ai/OpenAiClient.cs @@ -1,10 +1,7 @@ using System.Text.Json; -using System.Text.Json.Serialization; using CSharpFunctionalExtensions; -using My1kWordsEe.Models; - using OpenAI.Chat; namespace My1kWordsEe.Services @@ -111,85 +108,5 @@ public static async Task> GetDallEPrompt(this OpenAiClient openAi MaxTokens = 400, }); } - - public static async Task> GetWordMetadata( - this OpenAiClient openAiClient, - string eeWord, - string? comment = null) - { - const string prompt = - "Teie sisend on eestikeelne sõna (ja selle sõna valikuline selgitus).\n" + - "Kui antud sõna ei ole eestikeelne, tagasta 404\n" + - "Teie väljund on sõna metaandmed JSON-is vastavalt antud lepingule:\n" + - "```\n{\n" + - "\"ee_word\": \"\",\n" + - "\"en_word\": \"\",\n" + - "\"en_words\": [],\n" + - "\"en_explanation\": \"\",\n" + - "\"ee_explanation\": \"\"\n" + - "}\n```\n"; - - var response = await openAiClient.CompleteAsync( - prompt, - string.IsNullOrEmpty(comment) - ? eeWord - : $"{eeWord} ({comment})", - new ChatCompletionOptions - { - ResponseFormat = ChatResponseFormat.JsonObject, - Temperature = 0.333f - }); - - if (response.IsFailure) - { - return Result.Failure(response.Error); - } - - // could be ommited if we integrate an EE dictionary within the app - if (response.Value.Contains("404")) - { - return Result.Failure("Not an Estonian word"); - } - - openAiClient.ParseJsonResponse(response).Deconstruct( - out bool _, - out bool isParsingError, - out WordMetadata wordMetadata, - out string parsingError); - - if (isParsingError) - { - return Result.Failure(parsingError); - } - - return Result.Success(new SampleWord - { - EeWord = wordMetadata.EeWord, - EnWord = wordMetadata.EnWord, - EnWords = wordMetadata.EnWords, - EnExplanation = wordMetadata.EnExplanation, - EeExplanation = wordMetadata.EeExplanation, - }); - } - - private class WordMetadata - { - [JsonPropertyName("ee_word")] - public required string EeWord { get; set; } - - [JsonPropertyName("en_word")] - public required string EnWord { get; set; } - - [JsonPropertyName("en_explanation")] - public required string EnExplanation { get; set; } - - [JsonPropertyName("ee_explanation")] - public required string EeExplanation { get; set; } - - [JsonPropertyName("en_words")] - public required string[] EnWords { get; set; } = Array.Empty(); - } } - - } diff --git a/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs b/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs index c5085a0..7bd0ff8 100644 --- a/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs +++ b/My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs @@ -12,10 +12,10 @@ public class AddSampleSentenceCommand { public const int MaxSamples = 6; - private readonly AzureStorageClient azureBlobService; - private readonly OpenAiClient openAiService; + private readonly AzureStorageClient azureBlobClient; + private readonly OpenAiClient openAiClient; private readonly AddAudioCommand addAudioCommand; - private readonly StabilityAiClient stabilityAiService; + private readonly StabilityAiClient stabilityAiClient; public AddSampleSentenceCommand( AzureStorageClient azureBlobService, @@ -23,10 +23,10 @@ public AddSampleSentenceCommand( AddAudioCommand createAudioCommand, StabilityAiClient stabilityAiService) { - this.azureBlobService = azureBlobService; - this.openAiService = openAiService; + this.azureBlobClient = azureBlobService; + this.openAiClient = openAiService; this.addAudioCommand = createAudioCommand; - this.stabilityAiService = stabilityAiService; + this.stabilityAiClient = stabilityAiService; } public async Task> Invoke(SampleWord word) @@ -69,15 +69,15 @@ public async Task> Invoke(SampleWord word) }).ToArray() }; - return (await this.azureBlobService + return (await this.azureBlobClient .SaveWordData(updatedWordData)) .Bind(r => Result.Success(updatedWordData)); } private Task> GenerateImage(Sentence sentence) => - this.openAiService.GetDallEPrompt(sentence.En).Bind( - this.stabilityAiService.GenerateImage).Bind( - this.azureBlobService.SaveImage); + this.openAiClient.GetDallEPrompt(sentence.En).Bind( + this.stabilityAiClient.GenerateImage).Bind( + this.azureBlobClient.SaveImage); private Task> GenerateSpeech(Sentence sentence) => this.addAudioCommand.Invoke(sentence.Ee); @@ -111,7 +111,7 @@ private async Task> GetSampleSentence(SampleWord word) word.EnExplanation }); - var result = await this.openAiService.CompleteJsonAsync(prompt, input, temperature: 0.7f); + var result = await this.openAiClient.CompleteJsonAsync(prompt, input, temperature: 0.7f); return result; } diff --git a/My1kWordsEe/Services/Cqs/AddSampleWordCommand.cs b/My1kWordsEe/Services/Cqs/AddSampleWordCommand.cs index de7995a..e1e717f 100644 --- a/My1kWordsEe/Services/Cqs/AddSampleWordCommand.cs +++ b/My1kWordsEe/Services/Cqs/AddSampleWordCommand.cs @@ -1,14 +1,19 @@ -using CSharpFunctionalExtensions; +using System.Text.Json; +using System.Text.Json.Serialization; + +using CSharpFunctionalExtensions; using My1kWordsEe.Models; using My1kWordsEe.Services.Db; +using OpenAI.Chat; + namespace My1kWordsEe.Services.Cqs { public class AddSampleWordCommand { - private readonly OpenAiClient openAiService; - private readonly AzureStorageClient azureBlobService; + private readonly OpenAiClient openAiClient; + private readonly AzureStorageClient azureBlobClient; private readonly AddAudioCommand addAudioCommand; public AddSampleWordCommand( @@ -16,8 +21,8 @@ public AddSampleWordCommand( AzureStorageClient azureBlobService, AddAudioCommand createAudioCommand) { - this.azureBlobService = azureBlobService; - this.openAiService = openAiService; + this.azureBlobClient = azureBlobService; + this.openAiClient = openAiService; this.addAudioCommand = createAudioCommand; } @@ -28,7 +33,7 @@ public async Task> Invoke(string eeWord, string? comment = nu return Result.Failure("Not an Estonian word"); } - (await openAiService.GetWordMetadata(eeWord, comment)).Deconstruct( + (await this.GetWordMetadata(eeWord, comment)).Deconstruct( out bool _, out bool isAiFailure, out SampleWord sampleWord, @@ -48,8 +53,96 @@ public async Task> Invoke(string eeWord, string? comment = nu ? sampleWord with { EeAudioUrl = audioUri } : sampleWord; - return (await azureBlobService.SaveWordData(sampleWord)) + return (await azureBlobClient.SaveWordData(sampleWord)) .Bind(_ => Result.Of(sampleWord)); } + + private async Task> GetWordMetadata( + string eeWord, + string? comment = null) + { + const string prompt = + "Antud eestikeelse sõna kohta tuleb esitada metaandmed\n" + + + "Teie sisend on JSON-objekt:" + + "```\n{\n" + + "\"EeWord\": \"\", " + + "\"Comment\": \"\n" + + "}\n```\n" + + + "Kui antud sõna ei ole eestikeelne, tagasta 404\n" + + "Teie väljund on sõna metaandmed JSON-is vastavalt antud lepingule:\n" + + "```\n{\n" + + "\"ee_word\": \"\",\n" + + "\"en_word\": \"\",\n" + + "\"en_words\": [],\n" + + "\"en_explanation\": \"\",\n" + + "\"ee_explanation\": \"\"\n" + + "}\n```\n"; + + var input = JsonSerializer.Serialize(new + { + EeWord = eeWord, + Comment = comment + }); + + var response = await this.openAiClient.CompleteAsync( + prompt, + input, + new ChatCompletionOptions + { + ResponseFormat = ChatResponseFormat.JsonObject, + Temperature = 0.333f + }); + + if (response.IsFailure) + { + return Result.Failure(response.Error); + } + + // could be ommited if we integrate an EE dictionary within the app + if (response.Value.Contains("404")) + { + return Result.Failure("Not an Estonian word"); + } + + openAiClient.ParseJsonResponse(response).Deconstruct( + out bool _, + out bool isParsingError, + out WordMetadata wordMetadata, + out string parsingError); + + if (isParsingError) + { + return Result.Failure(parsingError); + } + + return Result.Success(new SampleWord + { + EeWord = wordMetadata.EeWord, + EnWord = wordMetadata.EnWord, + EnWords = wordMetadata.EnWords, + EnExplanation = wordMetadata.EnExplanation, + EeExplanation = wordMetadata.EeExplanation, + }); + } + + private class WordMetadata + { + [JsonPropertyName("ee_word")] + public required string EeWord { get; set; } + + [JsonPropertyName("en_word")] + public required string EnWord { get; set; } + + [JsonPropertyName("en_explanation")] + public required string EnExplanation { get; set; } + + [JsonPropertyName("ee_explanation")] + public required string EeExplanation { get; set; } + + [JsonPropertyName("en_words")] + public required string[] EnWords { get; set; } = Array.Empty(); + } } } diff --git a/My1kWordsEe/Services/Cqs/ValidateSampleWordCommand.cs b/My1kWordsEe/Services/Cqs/ValidateSampleWordCommand.cs index ced3c4a..65f35f9 100644 --- a/My1kWordsEe/Services/Cqs/ValidateSampleWordCommand.cs +++ b/My1kWordsEe/Services/Cqs/ValidateSampleWordCommand.cs @@ -1,4 +1,6 @@ -using CSharpFunctionalExtensions; +using System.Text.Json; + +using CSharpFunctionalExtensions; using My1kWordsEe.Models; @@ -34,12 +36,12 @@ public async Task> Invoke(SampleWord sample) "\"EeExplanationMessage\": \"\"\n" + "}\n```\n"; - var input = - "{" + - " \"EeWord\": \"" + sample.EeWord + "\"," + - " \"EnWord\": \"" + sample.EnWord + "\"," + - " \"EnExplanation\": \"" + sample.EnExplanation + "\"" + - "}"; + var input = JsonSerializer.Serialize(new + { + sample.EeWord, + sample.EnWord, + sample.EnExplanation + }); var result = await this.openAiClient.CompleteJsonAsync(prompt, input); From 9f1af3ca86e2a40e2bff3bef74a1023a4154f686 Mon Sep 17 00:00:00 2001 From: NICK BRAIN Date: Sat, 7 Dec 2024 19:27:58 +0200 Subject: [PATCH 3/3] + RedoSample uses validation --- My1kWordsEe/Components/Pages/WordPage.razor | 6 +++++- My1kWordsEe/Services/Cqs/RedoSampleWordCommand.cs | 6 +++--- My1kWordsEe/ee1k.json | 6 +++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/My1kWordsEe/Components/Pages/WordPage.razor b/My1kWordsEe/Components/Pages/WordPage.razor index a27e5b1..af10a4b 100644 --- a/My1kWordsEe/Components/Pages/WordPage.razor +++ b/My1kWordsEe/Components/Pages/WordPage.razor @@ -23,6 +23,7 @@ @inject RemoveFromFavoritesCommand RemoveFromFavoritesCommand @inject DeleteSampleSentenceCommand DeleteSampleSentenceCommand @inject RedoSampleWordCommand RedoSampleWordCommand +@inject ValidateSampleWordCommand ValidateSampleWordCommand @code { @@ -145,7 +146,10 @@ if (confirmation) { PreloadService.Show(SpinnerColor.Light, "Saving data..."); - var redoResult = await this.RedoSampleWordCommand.Invoke(Value.EeWord); + + var redoResult = await this.ValidateSampleWordCommand.Invoke(Value).Bind(r => + this.RedoSampleWordCommand.Invoke(Value.EeWord, r.EeExplanationMessage)); + if (redoResult.IsSuccess) { WordMetadata = redoResult; diff --git a/My1kWordsEe/Services/Cqs/RedoSampleWordCommand.cs b/My1kWordsEe/Services/Cqs/RedoSampleWordCommand.cs index 2f3fca2..daa5954 100644 --- a/My1kWordsEe/Services/Cqs/RedoSampleWordCommand.cs +++ b/My1kWordsEe/Services/Cqs/RedoSampleWordCommand.cs @@ -31,7 +31,7 @@ public async Task> Invoke(string eeWord, string? comment = nu (await azureBlobService.GetWordData(eeWord)).Deconstruct( out bool _, out bool isBlobAccessFailure, - out Maybe savedWord, + out Maybe existingWordData, out string blobAccessError); if (isBlobAccessFailure) @@ -41,9 +41,9 @@ public async Task> Invoke(string eeWord, string? comment = nu var redoTask = this.addSampleWordCommand.Invoke(eeWord, comment); - if (savedWord.HasValue) + if (existingWordData.HasValue) { - await Parallel.ForEachAsync(savedWord.Value.Samples, async (sample, ct) => + await Parallel.ForEachAsync(existingWordData.Value.Samples, async (sample, ct) => { if (ct.IsCancellationRequested) { return; } await deleteSampleSentenceCommand.Invoke(sample); diff --git a/My1kWordsEe/ee1k.json b/My1kWordsEe/ee1k.json index 4d5935c..4dbb1b6 100644 --- a/My1kWordsEe/ee1k.json +++ b/My1kWordsEe/ee1k.json @@ -2227,9 +2227,9 @@ }, { "EeWord": "eks", - "EnWord": "ex", - "EnWords": [ "formerly", "was", "in the past" ], - "EnExplanation": "A word used to indicate something that was or someone who was in a previous state or role, often referring to a former partner or spouse." + "EnWord": "isn't it", + "EnWords": [ "right", "isn't that so", "indeed" ], + "EnExplanation": "The word 'eks' is often used in Estonian as a tag question, similar to 'isn't it?' or 'right?', or as an interjection meaning 'indeed' or 'really'." }, { "EeWord": "ringi",