using System.Text; using System.Text.RegularExpressions; namespace AIStudio.Assistants.BeyondFactsLab; /// /// A person whom the research on the participants of the BeyondFacts Lab lists as taking part in the event. /// /// /// The agent of the topic "Participants" gives every person a heading of the third level with nothing but the name, see /// Prompts/research/participants.md. The lab reads these headings once, when it stores the research, and keeps the list /// as it is from then on, see BflResearch.Participants. /// public sealed partial class BflParticipant { /// /// How many characters a name has at most. A longer heading is no name. /// public const int MAX_NAME_CHARACTERS = 200; /// /// How many people a list holds at most. The prompt asks for 40 at most; the rest is left out. /// public const int MAX_PARTICIPANTS = 50; /// /// Gets or sets the name of the person, as the heading gives it. /// public string Name { get; set; } = string.Empty; /// /// Gets or sets what the list says about the person below the heading, in Markdown: the role at the event, the /// institution or function, and the source. /// public string Entry { get; set; } = string.Empty; /// /// Tells whether this is the person with a name, regardless of case and of the spaces between the words. /// /// The name. public bool HasName(string name) => string.Equals(NameKey(this.Name), NameKey(name), StringComparison.Ordinal); /// /// Reads the people from the answer of the agent of the topic "Participants". /// /// /// Every heading of the third level starts a person; a heading of a higher level ends it, one of a lower level /// belongs to its entry. Markdown in a heading, such as emphasis or a link, is taken off. A heading which is empty /// or too long is no person, and a name which comes twice counts once, with its first entry. /// /// The answer, with its lines ending in LF. /// The people, in the order of the answer. public static IReadOnlyList ListIn(string text) { var participants = new List(); var keys = new HashSet(StringComparer.Ordinal); BflParticipant? current = null; var entry = new StringBuilder(); foreach (var line in text.ReplaceLineEndings("\n").Split('\n')) { var heading = HeadingPattern().Match(line); if (!heading.Success || heading.Groups["level"].Length > 3) { if (current is not null) entry.Append(line).Append('\n'); continue; } Finish(); if (heading.Groups["level"].Length < 3 || participants.Count >= MAX_PARTICIPANTS) continue; var name = CleanName(heading.Groups["text"].Value); if (name.Length is 0 or > MAX_NAME_CHARACTERS || !keys.Add(NameKey(name))) continue; current = new BflParticipant { Name = name }; participants.Add(current); } Finish(); return participants; void Finish() { if (current is not null) current.Entry = entry.ToString().Trim(); current = null; entry.Clear(); } } /// /// Gets what decides whether two names are the same: the words in capitals, separated by one space. /// /// The name. public static string NameKey(string name) => string.Join(' ', name.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)).ToUpperInvariant(); /// /// Takes the Markdown off a heading and joins its words with single spaces. /// /// The text of the heading. private static string CleanName(string heading) { var cleaned = MarkdownLinkPattern().Replace(heading, "${label}"); cleaned = MarkupPattern().Replace(cleaned, string.Empty); return string.Join(' ', cleaned.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); } /// /// A heading in Markdown, with its level and its text. /// [GeneratedRegex(@"^[ \t]{0,3}(?#{1,6})[ \t]+(?.*?)[ \t#]*$", RegexOptions.CultureInvariant)] private static partial Regex HeadingPattern(); /// /// A link in Markdown, whose text stays. /// [GeneratedRegex(@"\[(?