Skip to content

Commit

Permalink
Word context improvements (#129)
Browse files Browse the repository at this point in the history
* + GetSampleSentence json input
* + GetWordMetadata json input
* + RedoSample uses validation
  • Loading branch information
NicklausBrain authored Dec 7, 2024
1 parent 56ee7a3 commit afc82f8
Show file tree
Hide file tree
Showing 7 changed files with 181 additions and 146 deletions.
6 changes: 5 additions & 1 deletion My1kWordsEe/Components/Pages/WordPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
@inject RemoveFromFavoritesCommand RemoveFromFavoritesCommand
@inject DeleteSampleSentenceCommand DeleteSampleSentenceCommand
@inject RedoSampleWordCommand RedoSampleWordCommand
@inject ValidateSampleWordCommand ValidateSampleWordCommand


@code {
Expand Down Expand Up @@ -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;
Expand Down
113 changes: 2 additions & 111 deletions My1kWordsEe/Services/Ai/OpenAiClient.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;

using CSharpFunctionalExtensions;

using My1kWordsEe.Models;

using OpenAI.Chat;

namespace My1kWordsEe.Services
Expand Down Expand Up @@ -49,11 +46,12 @@ public async Task<Result<string>> CompleteAsync(string instructions, string inpu
}
}

public async Task<Result<T>> CompleteJsonAsync<T>(string instructions, string input)
public async Task<Result<T>> CompleteJsonAsync<T>(string instructions, string input, float? temperature = null)
{
var response = await this.CompleteAsync(instructions, input, new ChatCompletionOptions
{
ResponseFormat = ChatResponseFormat.JsonObject,
Temperature = temperature
});

if (response.IsFailure)
Expand Down Expand Up @@ -110,112 +108,5 @@ public static async Task<Result<string>> GetDallEPrompt(this OpenAiClient openAi
MaxTokens = 400,
});
}

public static async Task<Result<SampleWord>> 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\": \"<antud sõna>\",\n" +
"\"en_word\": \"<english translation>\",\n" +
"\"en_words\": [<array of alternative english translations if applicable>],\n" +
"\"en_explanation\": \"<explanation of the word meaning in english>\",\n" +
"\"ee_explanation\": \"<sõna tähenduse seletus eesti keeles>\"\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<SampleWord>(response.Error);
}

// could be ommited if we integrate an EE dictionary within the app
if (response.Value.Contains("404"))
{
return Result.Failure<SampleWord>("Not an Estonian word");
}

openAiClient.ParseJsonResponse<WordMetadata>(response).Deconstruct(
out bool _,
out bool isParsingError,
out WordMetadata wordMetadata,
out string parsingError);

if (isParsingError)
{
return Result.Failure<SampleWord>(parsingError);
}

return Result.Success(new SampleWord
{
EeWord = wordMetadata.EeWord,
EnWord = wordMetadata.EnWord,
EnWords = wordMetadata.EnWords,
EnExplanation = wordMetadata.EnExplanation,
EeExplanation = wordMetadata.EeExplanation,
});
}

public static async Task<Result<Sentence>> 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: <sõna> (<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\": \"<näide eesti keeles>\", \"en_sentence\": \"<näide inglise keeles>\"" +
"\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<Sentence>(prompt, $"{eeWord} (${explanation})");
}

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<string>();
}
}

public class Sentence
{
[JsonPropertyName("ee_sentence")]
public required string Ee { get; set; }

[JsonPropertyName("en_sentence")]
public required string En { get; set; }
}
}
72 changes: 58 additions & 14 deletions My1kWordsEe/Services/Cqs/AddSampleSentenceCommand.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;

using CSharpFunctionalExtensions;

using My1kWordsEe.Models;
Expand All @@ -9,21 +12,21 @@ 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,
OpenAiClient openAiService,
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<Result<SampleWord>> Invoke(SampleWord word)
Expand All @@ -33,10 +36,8 @@ public async Task<Result<SampleWord>> Invoke(SampleWord word)
return Result.Failure<SampleWord>($"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<SampleWord>($"Sentence generation failed: {sentence.Error}");
Expand Down Expand Up @@ -68,17 +69,60 @@ public async Task<Result<SampleWord>> Invoke(SampleWord word)
}).ToArray()
};

return (await this.azureBlobService
return (await this.azureBlobClient
.SaveWordData(updatedWordData))
.Bind(r => Result.Success(updatedWordData));
}

private Task<Result<Uri>> 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<Result<Uri>> GenerateSpeech(Sentence sentence) =>
this.addAudioCommand.Invoke(sentence.Ee);

private async Task<Result<Sentence>> 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\": \"<eestikeelne sõna>\", " +
"\"EnWord\": \"<default english translation>\n" +
"\"EnExplanation\": \"<explanation of the estonian word in english>\n" +
"}\n```\n" +

"Sinu sisend on üks eestikeelne sõna ja selle rakenduse kontekst: <sõna> (<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\": \"<näide eesti keeles>\", \"en_sentence\": \"<näide inglise keeles>\"" +
"\n}\n```\n";

var input = JsonSerializer.Serialize(new
{
word.EeWord,
word.EnWord,
word.EnExplanation
});

var result = await this.openAiClient.CompleteJsonAsync<Sentence>(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; }
}
}
}
Loading

0 comments on commit afc82f8

Please sign in to comment.