Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Word context improvements #129

Merged
merged 3 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -58,7 +59,7 @@
var commonSample = this.EnsureWordCommand.Invoke(EeWord);
if (IsLoggedIn)
{
var user = await UserAccessor.GetRequiredUserAsync(User);

Check warning on line 62 in My1kWordsEe/Components/Pages/WordPage.razor

View workflow job for this annotation

GitHub Actions / test

Possible null reference argument for parameter 'user' in 'Task<ApplicationUser> IdentityUserAccessor.GetRequiredUserAsync(ClaimsPrincipal user)'.
var favorites = GetFavoritesQuery.Invoke(user.Id);
await Task.WhenAll(commonSample, favorites);

Expand All @@ -74,7 +75,7 @@
private async Task AddToFavorites(dynamic favorite)
{
PreloadService.Show(SpinnerColor.Light, "Saving data...");
var user = await UserAccessor.GetRequiredUserAsync(User);

Check warning on line 78 in My1kWordsEe/Components/Pages/WordPage.razor

View workflow job for this annotation

GitHub Actions / test

Possible null reference argument for parameter 'user' in 'Task<ApplicationUser> IdentityUserAccessor.GetRequiredUserAsync(ClaimsPrincipal user)'.
Favorites = await AddToFavoritesCommand.Invoke(user.Id, favorite);
await Task.Delay(300);
PreloadService.Hide();
Expand All @@ -84,7 +85,7 @@
private async Task RemoveFromFavorites(dynamic favorite)
{
PreloadService.Show(SpinnerColor.Light, "Saving data...");
var user = await UserAccessor.GetRequiredUserAsync(User);

Check warning on line 88 in My1kWordsEe/Components/Pages/WordPage.razor

View workflow job for this annotation

GitHub Actions / test

Possible null reference argument for parameter 'user' in 'Task<ApplicationUser> IdentityUserAccessor.GetRequiredUserAsync(ClaimsPrincipal user)'.
Favorites = await RemoveFromFavoritesCommand.Invoke(user.Id, favorite);
PreloadService.Hide();
await Task.Delay(300);
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 @@ -32,7 +29,7 @@
return Result.Failure<string>("Open AI API key is missing");
};

ChatClient client = new(model: Model, this.config[ApiSecretKey]);

Check warning on line 32 in My1kWordsEe/Services/Ai/OpenAiClient.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference argument for parameter 'key' in 'ApiKeyCredential.implicit operator ApiKeyCredential(string key)'.

try
{
Expand All @@ -49,11 +46,12 @@
}
}

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 @@
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
Loading