using System.Text; namespace AIStudio.Assistants.BeyondFactsLab; /// /// Puts together the system prompts of the BeyondFacts Lab. /// /// /// The same parts, forms, and materials always give the same prompt, byte by byte, on every system. The parts which /// stay the same during a conversation come first, the core and the guide, then the material, which changes seldom, /// and the details last, so that providers which cache the beginning of a prompt can do so. The texts this class adds /// itself are German, like the prompts. /// public static class BflPromptComposer { /// /// The line break of the prompt, the same on every system. /// private const char LINE_BREAK = '\n'; /// /// Puts together the system prompt of a step. /// /// The texts of the prompt. /// The step the chat belongs to. /// The forms of the preparation, by their steps; a step this version cannot open is missing. /// What the prompt carries of the material of the preparation, see BflMaterialBudget. /// The name of the language the model answers in, e.g., the language of the app. /// The system prompt. /// The core prompt has a placeholder without a value. public static string Compose(BflPromptParts parts, BflStep step, IReadOnlyDictionary forms, IReadOnlyList materials, string responseLanguage) => BflPromptTemplate.Fill(parts.Core, new Dictionary(StringComparer.Ordinal) { [BflPromptTemplate.RESPONSE_LANGUAGE] = responseLanguage, [BflPromptTemplate.ADDRESS_FORM] = parts.Address.Trim(), [BflPromptTemplate.KNOWLEDGE] = parts.Knowledge.Trim(), [BflPromptTemplate.STEP] = parts.Step.Trim(), [BflPromptTemplate.MATERIALS] = Materials(materials), [BflPromptTemplate.DETAILS] = Details(step, forms), [BflPromptTemplate.MISSING_DETAILS] = MissingDetails(step, forms) }).ReplaceLineEndings(LINE_BREAK.ToString()); /// /// Puts together the system prompt of the conversation about whether the lab fits a task. /// /// /// It knows no preparation and leaves out the guide, so that it stays short. /// /// The texts of the prompt. /// The name of the language the model answers in, e.g., the language of the app. /// The system prompt. /// The prompt has a placeholder without a value. public static string ComposeOnboarding(BflOnboardingPromptParts parts, string responseLanguage) => BflPromptTemplate.Fill(parts.Onboarding, new Dictionary(StringComparer.Ordinal) { [BflPromptTemplate.RESPONSE_LANGUAGE] = responseLanguage, [BflPromptTemplate.ADDRESS_FORM] = parts.Address.Trim() }).ReplaceLineEndings(LINE_BREAK.ToString()); /// /// Lists the material of the preparation, each with its name and as much of its text as fits, or for an image, /// whether it goes along. /// /// /// The model learns which material is shortened, so that it neither takes the beginning for the whole nor /// guesses what follows, and which text is the transcript of a recording, whose spelling may be off. /// /// What the prompt carries of each material, in the order the person added them. private static string Materials(IReadOnlyList materials) { if (materials.Count is 0) return "Die Person hat kein Material hinzugefügt."; var text = new StringBuilder(); for (var index = 0; index < materials.Count; index++) { var excerpt = materials[index]; var material = excerpt.Share.Material; if (index > 0) text.Append(LINE_BREAK); text.Append($"## Material {index + 1}: „{material.Name}“").Append(LINE_BREAK); if (material.Kind is BflMaterialKind.IMAGE) { text.Append(excerpt.Share.Image switch { BflImageDelivery.SENT => "(Ein Bild. Es liegt der ersten Nachricht dieses Gesprächs bei.)", BflImageDelivery.OVER_THE_LIMIT => "(Ein Bild. Das Modell nimmt nicht so viele Bilder auf einmal an. Du kennst nur seinen Namen.)", _ => "(Ein Bild. Das Modell kann keine Bilder sehen. Du kennst nur seinen Namen.)", }).Append(LINE_BREAK); continue; } if (material.Kind is BflMaterialKind.RECORDING) text.Append("(Das Transkript einer Aufnahme. Namen und Fachbegriffe können falsch geschrieben sein.)").Append(LINE_BREAK); if (excerpt.Text.Length is 0) { text.Append("(Für dieses Material reicht der Platz im Kontext des Modells nicht. Du kennst nur seinen Namen.)").Append(LINE_BREAK); continue; } if (material.IsShortened) text.Append("(Das Lab hat nur den Anfang dieses Materials übernommen. Den Rest kennst du nicht.)").Append(LINE_BREAK); if (!excerpt.Share.IsComplete) text.Append($"(Gekürzt: Hier steht nur der Anfang, etwa {excerpt.Share.Percent} % des Materials. Für mehr reicht der Platz im Kontext des Modells nicht.)").Append(LINE_BREAK); text.Append(LINE_BREAK).Append(excerpt.Text.Trim()).Append(LINE_BREAK); } return text.ToString().TrimEnd(); } /// /// Lists what the person entered into the forms, step by step. /// /// /// Something the person confided, such as their personal involvement, reaches only the chat of its own step: /// no other step may confront them with it by surprise. /// /// The step the chat belongs to. /// The forms of the preparation, by their steps. private static string Details(BflStep step, IReadOnlyDictionary forms) { var text = new StringBuilder(); foreach (var candidate in BflStep.All) { if (!forms.TryGetValue(candidate, out var form)) continue; var lines = new List(); foreach (var field in BflFieldCatalog.ForStep(candidate)) { if (field.IsPersonalDisclosure && candidate != step) continue; var value = ValueOf(field, form); if (value.Length is 0) continue; var marker = field.IsPersonalDisclosure ? " (persönliche Angabe)" : string.Empty; lines.Add($"- {field.Label}{marker}: {value.Replace("\n", "\n ", StringComparison.Ordinal)}"); } if (lines.Count is 0) continue; if (text.Length > 0) text.Append(LINE_BREAK); text.Append("## ").Append(StepHeading(candidate, step)).Append(LINE_BREAK); foreach (var line in lines) text.Append(line).Append(LINE_BREAK); } return text.Length is 0 ? "Die Person hat noch keine Angaben gemacht." : text.ToString().TrimEnd(); } /// /// Lists the key details the forms still lack, step by step. /// /// The step the chat belongs to. /// The forms of the preparation, by their steps; a missing form counts as empty. private static string MissingDetails(BflStep step, IReadOnlyDictionary forms) { var lines = new List(); foreach (var candidate in BflStep.All) { var missing = BflMissingDetails.Of(candidate, forms.GetValueOrDefault(candidate) ?? new BflStepFormData()); if (missing.Count > 0) lines.Add($"- {StepHeading(candidate, step)}: {string.Join("; ", missing.Select(field => field.Label))}"); } return lines.Count is 0 ? "Es fehlen keine wichtigen Angaben." : $"Diese wichtigen Angaben fehlen noch:{LINE_BREAK}{string.Join(LINE_BREAK, lines)}"; } /// /// Gets the value of a field as the model reads it: the labels of chosen options, or the text with its lines. /// /// The field. /// The form which holds the value. /// The value, empty when the field has none. private static string ValueOf(BflFieldDefinition field, BflStepFormData form) { if (field.Kind is BflFieldKind.CHOICE or BflFieldKind.MULTI_CHOICE) { // An option this version does not know, e.g., one a newer version added, has no label to show: var chosen = form.GetChoices(field.Id); return string.Join(", ", field.Options.Where(option => chosen.Contains(option.Id)).Select(option => option.Label)); } return form.Get(field.Id).Trim().ReplaceLineEndings(LINE_BREAK.ToString()); } /// /// Gets the heading of a step in the details, which marks the step the chat belongs to. /// /// The step to name. /// The step the chat belongs to. private static string StepHeading(BflStep candidate, BflStep step) => candidate == step ? $"{candidate.Title} (dieser Schritt)" : candidate.Title; }