using AIStudio.Models; namespace AIStudio.Assistants.BeyondFactsLab; /// /// Decides how much of the material of a preparation the prompt of a step carries. /// /// /// /// Every request of a step carries the guide and the material in its system prompt, so the material must leave the /// conversation room in the context of the model. It gets half of what the prompt without material leaves free, and /// the lab shares that out evenly: a short material comes whole, and the long ones are shortened alike. The budget /// counts characters, about four per token like the limits of the prompts, so that putting a prompt together needs /// no tokenizer; the token budget of the chat still counts what a request really carries. /// /// /// A shortened material is the beginning of the text which passed the prompt injection filter when the person added /// it. It does not pass the filter again: cutting off the end of a text adds nothing which the filter did not see, /// unlike IRetrievalContextExtensions.ShortenToFitAsync, which cuts the unfiltered text of a data source. /// /// /// An image takes no characters: as many images as the model takes at once go along with the first message of the /// conversation, and each takes TOKENS_PER_IMAGE from the free room before the text gets its half. /// /// public static class BflMaterialBudget { /// /// The share of what the prompt without material leaves free in the context window, which the material may take. /// /// /// The other half stays for the conversation, so that a long material does not end it after a few messages. /// public const double SHARE_OF_THE_FREE_CONTEXT = 0.5; /// /// How many characters the budget counts for one token, about as many as a German text takes. /// public const int CHARACTERS_PER_TOKEN = 4; /// /// How many characters the material takes at most, whatever the model reads at once. /// /// /// About 100,000 tokens. Every message of a conversation carries the material again, so a book in the material /// of a model with a huge context window would make every message slow and expensive. /// public const int MAX_CHARACTERS = 400_000; /// /// How many characters the material takes when nobody knows the context window of the model. /// /// /// About 10,000 tokens: together with the guide and a conversation, that fits a model which reads 64,000 tokens /// at once. The token budget of the chat tells the person when a conversation grows too long. /// public const int CHARACTERS_WITHOUT_A_KNOWN_CONTEXT_WINDOW = 40_000; /// /// How much of what fits a shortened material gives up at most to end at a line end, as a share. /// private const double MAX_SHARE_GIVEN_UP_FOR_A_LINE_END = 0.1; /// /// How many tokens the lab expects one image to take. /// /// /// No provider tells beforehand. Anthropic counts about width × height / 750 tokens and first scales an image down /// to about 1.15 megapixels, which gives about 1,600 tokens at most; OpenAI counts at most about 1,100 for one image /// in high detail. The lab reserves the larger one for every image it sends. /// public const int TOKENS_PER_IMAGE = 1_600; /// /// Gets how many characters the text of the material of a preparation may take in the prompt of a step. /// /// The context window of the model which answers. /// How many characters the system prompt has without the material. /// How many images of the material go along with the request; each takes room first. /// The characters, between 0 and MAX_CHARACTERS. public static int CharactersFor(ContextWindow contextWindow, int promptCharacters, int sentImages = 0) { if (!contextWindow.IsKnown) return CHARACTERS_WITHOUT_A_KNOWN_CONTEXT_WINDOW; var freeTokens = (long)contextWindow.DefaultTokens - BflTokenBudget.RESERVE_TOKENS - (long)Math.Max(0, sentImages) * TOKENS_PER_IMAGE; var freeCharacters = freeTokens * CHARACTERS_PER_TOKEN - promptCharacters; return (int)Math.Clamp((long)(freeCharacters * SHARE_OF_THE_FREE_CONTEXT), 0, MAX_CHARACTERS); } /// /// Decides which images of the material go along with a request: as many as the model takes at once, in the order /// the person added them. /// /// The images of the material, in the order the person added them. /// How many images the model takes in one message; 0 when it sees none. /// The share of each image, in the same order. public static IReadOnlyList ShareImages(IReadOnlyList images, int imagesTheModelTakes) => [ .. images.Select((image, index) => new BflMaterialShare(image, 0, imagesTheModelTakes <= 0 ? BflImageDelivery.MODEL_SEES_NO_IMAGES : index < imagesTheModelTakes ? BflImageDelivery.SENT : BflImageDelivery.OVER_THE_LIMIT)) ]; /// /// Shares out the characters among the materials evenly: what a short material does not need goes to the others. /// /// The materials, in the order the person added them. /// How many characters the materials may take together. /// The share of each material, in the same order. public static IReadOnlyList Share(IReadOnlyList materials, int characters) { var shares = new int[materials.Count]; var remaining = Math.Max(0, characters); var open = materials.Count; foreach (var index in Enumerable.Range(0, materials.Count).OrderBy(index => materials[index].TextLength).ThenBy(index => index)) { var share = Math.Min(Math.Max(0, materials[index].TextLength), remaining / open); shares[index] = share; remaining -= share; open--; } return [.. materials.Select((material, index) => new BflMaterialShare(material, shares[index]))]; } /// /// Ends the beginning of a longer text at a line end, so that a shortened material keeps whole lines, such as /// the rows of a table. /// /// /// A text whose last line end lies too far from its end is cut where it ends, though never between the two halves /// of a character outside the Basic Multilingual Plane. /// /// The beginning of the text, as long as its share. /// The beginning, ending at a line end where possible. public static string EndAtLineEnd(string beginning) { var lineEnd = beginning.LastIndexOf('\n'); if (lineEnd >= 0 && lineEnd + 1 >= beginning.Length * (1 - MAX_SHARE_GIVEN_UP_FOR_A_LINE_END)) return beginning[..lineEnd].TrimEnd(); var end = beginning.Length; if (end > 0 && char.IsHighSurrogate(beginning[end - 1])) end--; return beginning[..end].TrimEnd(); } }