-
Notifications
You must be signed in to change notification settings - Fork 44
Add semantic model AI metadata scripts #31
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
Open
data-goblin
wants to merge
2
commits into
TabularEditor:master
Choose a base branch
from
data-goblin:kurt/ex004/semantic-model-ai-metadata-scripts
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
395 changes: 395 additions & 0 deletions
395
Semantic Model AI Metadata/Edit Semantic Model AI Instructions.csx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,395 @@ | ||
| #r "System.Drawing" | ||
|
|
||
| /* | ||
| * Title: Edit semantic model AI instructions | ||
| * | ||
| * Author: Kurt Buhler | ||
| * | ||
| * This Tabular Editor 3 Desktop script opens a GUI editor for semantic model | ||
| * AI instructions stored in culture linguistic metadata CustomInstructions. | ||
| * It uses the connected model, defaults to the en-US culture, and does not | ||
| * require a selected object. | ||
| * | ||
| * Provided as-is. This script edits culture linguistic metadata directly and is | ||
| * not an official supported Tabular Editor product feature. | ||
| */ | ||
|
|
||
| // TE3 Desktop GUI editor for semantic model AI instructions. | ||
| // Uses Model.Cultures["en-US"].Content -> CustomInstructions. | ||
|
|
||
| using System; | ||
| using System.Drawing; | ||
| using System.Linq; | ||
| using System.Reflection; | ||
| using System.Windows.Forms; | ||
| using Newtonsoft.Json; | ||
| using Newtonsoft.Json.Linq; | ||
| using TabularEditor.TOMWrapper; | ||
|
|
||
| try | ||
| { | ||
| AiInstructionsEditor.Run(Model); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| MessageBox.Show(AiInstructionsEditor.RootMessage(ex), "Semantic Model AI Instructions"); | ||
| } | ||
|
|
||
| public static class AiInstructionsEditor | ||
| { | ||
| private const string DefaultCultureName = "en-US"; | ||
| private const int InstructionsLimit = 10000; | ||
|
|
||
| public static void Run(TabularEditor.TOMWrapper.Model model) | ||
| { | ||
| ScriptHelper.WaitFormVisible = false; | ||
|
|
||
| if (model == null) | ||
| { | ||
| MessageBox.Show("Open or connect to a model before running this script.", "Semantic Model AI Instructions"); | ||
| return; | ||
| } | ||
|
|
||
| string startupWarning; | ||
| var culture = EnsureCulture(model, DefaultCultureName, out startupWarning); | ||
| if (culture == null) | ||
| { | ||
| MessageBox.Show("Could not find or create the en-US culture.", "Semantic Model AI Instructions"); | ||
| return; | ||
| } | ||
|
|
||
| using (var form = new Form()) | ||
| using (var editor = ScriptTextEditor.Create(true)) | ||
| { | ||
| form.Text = "Semantic Model AI Instructions"; | ||
| form.StartPosition = FormStartPosition.CenterScreen; | ||
| form.AutoScaleMode = AutoScaleMode.Dpi; | ||
| form.Width = 980; | ||
| form.Height = 760; | ||
| form.MinimumSize = new Size(760, 520); | ||
|
|
||
| var font = new Font("Segoe UI", 9F); | ||
|
|
||
| var layout = new TableLayoutPanel | ||
| { | ||
| Dock = DockStyle.Fill, | ||
| ColumnCount = 1, | ||
| RowCount = 3, | ||
| Padding = new Padding(10) | ||
| }; | ||
| layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 34F)); | ||
| layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); | ||
| layout.RowStyles.Add(new RowStyle(SizeType.Absolute, 58F)); | ||
|
|
||
| var header = new Label | ||
| { | ||
| Dock = DockStyle.Fill, | ||
| AutoEllipsis = true, | ||
| TextAlign = ContentAlignment.MiddleLeft, | ||
| Font = font, | ||
| Text = "Model: " + model.Name + " Culture: " + culture.Name | ||
| }; | ||
|
|
||
| var editorPanel = new Panel | ||
| { | ||
| Dock = DockStyle.Fill, | ||
| BorderStyle = BorderStyle.FixedSingle, | ||
| Padding = new Padding(0) | ||
| }; | ||
| editorPanel.Controls.Add(editor.Control); | ||
|
|
||
| var bottom = new TableLayoutPanel | ||
| { | ||
| Dock = DockStyle.Fill, | ||
| ColumnCount = 2, | ||
| RowCount = 1, | ||
| Margin = new Padding(0), | ||
| Padding = new Padding(0, 8, 0, 4) | ||
| }; | ||
| bottom.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); | ||
| bottom.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); | ||
|
|
||
| var status = new Label | ||
| { | ||
| Dock = DockStyle.Fill, | ||
| AutoEllipsis = true, | ||
| TextAlign = ContentAlignment.MiddleLeft, | ||
| Font = font, | ||
| Margin = new Padding(0, 0, 8, 0) | ||
| }; | ||
|
|
||
| var wrapButton = NewFooterButton("Wrap", font); | ||
| var reloadButton = NewFooterButton("Reload", font); | ||
| var saveButton = NewFooterButton("Save", font); | ||
| var closeButton = NewFooterButton("Close", font); | ||
| closeButton.DialogResult = DialogResult.Cancel; | ||
|
|
||
| var buttonStrip = new FlowLayoutPanel | ||
| { | ||
| Dock = DockStyle.Fill, | ||
| AutoSize = true, | ||
| AutoSizeMode = AutoSizeMode.GrowAndShrink, | ||
| FlowDirection = FlowDirection.LeftToRight, | ||
| WrapContents = false, | ||
| Margin = new Padding(0), | ||
| Padding = new Padding(0) | ||
| }; | ||
| buttonStrip.Controls.Add(wrapButton); | ||
| buttonStrip.Controls.Add(reloadButton); | ||
| buttonStrip.Controls.Add(saveButton); | ||
| buttonStrip.Controls.Add(closeButton); | ||
|
|
||
| bottom.Controls.Add(status, 0, 0); | ||
| bottom.Controls.Add(buttonStrip, 1, 0); | ||
|
|
||
| layout.Controls.Add(header, 0, 0); | ||
| layout.Controls.Add(editorPanel, 0, 1); | ||
| layout.Controls.Add(bottom, 0, 2); | ||
| form.Controls.Add(layout); | ||
| form.CancelButton = closeButton; | ||
|
|
||
| Action refreshStatus = () => | ||
| { | ||
| var count = NormalizeForStorage(editor.Text).Length; | ||
| status.Text = count + " / " + InstructionsLimit + " characters" + | ||
| (string.IsNullOrWhiteSpace(startupWarning) ? "" : " " + startupWarning); | ||
| status.ForeColor = count > InstructionsLimit ? Color.Firebrick : | ||
| string.IsNullOrWhiteSpace(startupWarning) ? SystemColors.ControlText : Color.DarkGoldenrod; | ||
| saveButton.Enabled = count <= InstructionsLimit; | ||
| }; | ||
|
|
||
| Action load = () => | ||
| { | ||
| try | ||
| { | ||
| editor.Text = NormalizeForEditor(GetInstructions(culture)); | ||
| editor.SelectStart(); | ||
| refreshStatus(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| status.Text = RootMessage(ex); | ||
| status.ForeColor = Color.Firebrick; | ||
| } | ||
| }; | ||
|
|
||
| editor.TextChanged += (sender, args) => refreshStatus(); | ||
| wrapButton.Click += (sender, args) => editor.WordWrap = !editor.WordWrap; | ||
| reloadButton.Click += (sender, args) => load(); | ||
| saveButton.Click += (sender, args) => | ||
| { | ||
| try | ||
| { | ||
| var text = NormalizeForStorage(editor.Text); | ||
| if (text.Length > InstructionsLimit) | ||
| { | ||
| status.Text = "AI instructions must be 10000 characters or fewer."; | ||
| status.ForeColor = Color.Firebrick; | ||
| return; | ||
| } | ||
|
|
||
| SetInstructions(culture, text); | ||
| status.Text = "Saved to " + culture.Name + ". Save the model to persist."; | ||
| status.ForeColor = Color.ForestGreen; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| status.Text = RootMessage(ex); | ||
| status.ForeColor = Color.Firebrick; | ||
| } | ||
| }; | ||
| closeButton.Click += (sender, args) => form.Close(); | ||
|
|
||
| load(); | ||
| form.Shown += (sender, args) => editor.Focus(); | ||
| form.ShowDialog(); | ||
| } | ||
| } | ||
|
|
||
| private static Culture EnsureCulture(TabularEditor.TOMWrapper.Model model, string cultureName, out string warning) | ||
| { | ||
| warning = null; | ||
|
|
||
| if (model.Cultures.Contains(cultureName)) return model.Cultures[cultureName]; | ||
|
|
||
| try | ||
| { | ||
| return model.AddTranslation(cultureName); | ||
| } | ||
| catch | ||
| { | ||
| // Power BI Desktop-connected models may block AddTranslation. Fall back to TE's import helper. | ||
| } | ||
|
|
||
| try | ||
| { | ||
| if (TryImportEmptyCulture(model, cultureName) && model.Cultures.Contains(cultureName)) | ||
| { | ||
| warning = "Created " + cultureName + " culture."; | ||
| return model.Cultures[cultureName]; | ||
| } | ||
| } | ||
| catch | ||
| { | ||
| // Fall through to an existing culture or a controlled message. | ||
| } | ||
|
|
||
| var fallback = model.Cultures.FirstOrDefault(c => !string.IsNullOrWhiteSpace(c.Content)) | ||
| ?? model.Cultures.FirstOrDefault(); | ||
| if (fallback != null) | ||
| { | ||
| warning = "Could not create " + cultureName + "; using " + fallback.Name + "."; | ||
| return fallback; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| private static Button NewFooterButton(string text, Font font) | ||
| { | ||
| return new Button | ||
| { | ||
| Text = text, | ||
| Dock = DockStyle.Fill, | ||
| Font = font, | ||
| AutoSize = true, | ||
| AutoSizeMode = AutoSizeMode.GrowAndShrink, | ||
| Margin = new Padding(6, 2, 0, 2), | ||
| MinimumSize = new Size(96, 32), | ||
| Padding = new Padding(12, 0, 12, 0), | ||
| TextAlign = ContentAlignment.MiddleCenter, | ||
| UseVisualStyleBackColor = true | ||
| }; | ||
| } | ||
|
|
||
| private static bool TryImportEmptyCulture(TabularEditor.TOMWrapper.Model model, string cultureName) | ||
| { | ||
| var helperType = typeof(TabularEditor.TOMWrapper.Model).Assembly.GetType("TabularEditor.TOMWrapper.TabularCultureHelper"); | ||
| if (helperType == null) return false; | ||
|
|
||
| var method = helperType.GetMethod("ImportCulture", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); | ||
| if (method == null) return false; | ||
|
|
||
| var cultureJson = new JObject { ["name"] = cultureName }; | ||
| var result = method.Invoke(null, new object[] { cultureJson, model, false, true }); | ||
| return result is bool ok && ok; | ||
| } | ||
|
|
||
| private static string GetInstructions(Culture culture) | ||
| { | ||
| var payload = GetPayload(culture, false); | ||
| return (string)payload["CustomInstructions"] ?? ""; | ||
| } | ||
|
|
||
| private static void SetInstructions(Culture culture, string instructions) | ||
| { | ||
| var payload = GetPayload(culture, true); | ||
| payload["CustomInstructions"] = instructions ?? ""; | ||
| SavePayload(culture, payload); | ||
| } | ||
|
|
||
| private static JObject GetPayload(Culture culture, bool create) | ||
| { | ||
| if (!string.IsNullOrWhiteSpace(culture.Content)) | ||
| { | ||
| return JObject.Parse(culture.Content); | ||
| } | ||
|
|
||
| if (!create) return new JObject(); | ||
|
|
||
| return new JObject | ||
| { | ||
| ["Version"] = "4.2.0", | ||
| ["Language"] = culture.Name, | ||
| ["Entities"] = new JObject(), | ||
| ["Agents"] = new JObject | ||
| { | ||
| ["Internal"] = new JObject { ["Version"] = "1.1.0" } | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| private static void SavePayload(Culture culture, JObject payload) | ||
| { | ||
| culture.Content = payload.ToString(Formatting.Indented); | ||
| } | ||
|
|
||
| private static string NormalizeForEditor(string text) | ||
| { | ||
| return (text ?? "").Replace("\r\n", "\n").Replace("\r", "\n").Replace("\n", Environment.NewLine); | ||
| } | ||
|
|
||
| private static string NormalizeForStorage(string text) | ||
| { | ||
| return (text ?? "").Replace("\r\n", "\n").Replace("\r", "\n"); | ||
| } | ||
|
|
||
| public static string RootMessage(Exception ex) | ||
| { | ||
| if (ex == null) return ""; | ||
| while (ex.InnerException != null) ex = ex.InnerException; | ||
| return ex.Message; | ||
| } | ||
| } | ||
|
|
||
| public sealed class ScriptTextEditor : IDisposable | ||
| { | ||
| private readonly TextBox textBox; | ||
|
|
||
| public Control Control { get; private set; } | ||
| public event EventHandler TextChanged; | ||
|
|
||
| private ScriptTextEditor(TextBox textBox) | ||
| { | ||
| this.textBox = textBox; | ||
| Control = textBox; | ||
| Control.TextChanged += (sender, args) => TextChanged?.Invoke(this, EventArgs.Empty); | ||
| } | ||
|
|
||
| public static ScriptTextEditor Create(bool wordWrap) | ||
| { | ||
| var textBox = new TextBox | ||
| { | ||
| Dock = DockStyle.Fill, | ||
| Multiline = true, | ||
| ScrollBars = ScrollBars.Both, | ||
| WordWrap = wordWrap, | ||
| AcceptsReturn = true, | ||
| AcceptsTab = true, | ||
| Font = new Font("Consolas", 10F), | ||
| BorderStyle = BorderStyle.None | ||
| }; | ||
| return new ScriptTextEditor(textBox); | ||
| } | ||
|
|
||
| public string Text | ||
| { | ||
| get { return Control.Text ?? ""; } | ||
| set { Control.Text = value ?? ""; } | ||
| } | ||
|
|
||
| public bool WordWrap | ||
| { | ||
| get { return textBox.WordWrap; } | ||
| set | ||
| { | ||
| textBox.WordWrap = value; | ||
| } | ||
| } | ||
|
|
||
| public void Focus() | ||
| { | ||
| Control.Focus(); | ||
| } | ||
|
|
||
| public void SelectStart() | ||
| { | ||
| textBox.SelectionStart = 0; | ||
| textBox.SelectionLength = 0; | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| Control?.Dispose(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(applies to lines 265-269)
It's OK to keep this in, even though it's technically accessing internals, there's an argument to be made that the
ImportCulturemethod should be available for the public scripting API. I've added this to our internal backlog. Once that's implemented, we can then update this script to use the public surface instead of reflection.