Created
May 14, 2025 08:29
-
-
Save data-goblin/f03808e3353b2851cf5ef5760bf9c71d to your computer and use it in GitHub Desktop.
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
| #r "System.Net.Http" | |
| #r "System.Windows.Forms" | |
| using System; | |
| using System.IO; | |
| using System.Net.Http; | |
| using System.Text; | |
| using System.Linq; | |
| using System.Collections.Generic; | |
| using System.Windows.Forms; | |
| using System.Drawing; | |
| using Newtonsoft.Json; | |
| using Newtonsoft.Json.Linq; | |
| using TabularEditor.TOMWrapper; | |
| ScriptHelper.WaitFormVisible = false; | |
| // Define prompts at script level (without static modifier) | |
| string measurePrompt = "You are a data modeling expert. For the measure named \"{0}\" in the table \"{1}\", " + | |
| "create a clear, concise business description (as few bullet points as possible, maximum 3, where each bullet point is maximum 8-12 words) explaining what this measure represents. " + | |
| "Use proper bullet formatting with a new line for each bullet. " + | |
| "Do not include the measure name. Focus on business meaning."; | |
| string columnPrompt = "You are a data modeling expert. For the column named \"{0}\" in the table \"{1}\" with data type {2}, " + | |
| "create a clear, concise business description (as few bullet points as possible, maximum 3, where each bullet point is maximum 8-12 words) explaining what this column represents. " + | |
| "Use proper bullet formatting with a new line for each bullet. " + | |
| "Do not include the column name, only the bullet points. Focus on business meaning."; | |
| // Define constants without static modifier | |
| string apiUrl = "https://api.anthropic.com/v1/messages"; | |
| string claudeModel = "claude-3-7-sonnet-20250219"; | |
| int maxTokens = 300; | |
| // Main script | |
| void Main() | |
| { | |
| // Configure these settings | |
| bool dontOverwrite = false; // Set to true to skip objects with existing descriptions | |
| // Retrieve the API key from the C# script to use in the API call | |
| string apiKey = Environment.GetEnvironmentVariable("ClaudeAPI"); | |
| // If that didn't work, try user environment variables | |
| if (string.IsNullOrEmpty(apiKey)) { | |
| apiKey = Environment.GetEnvironmentVariable("ClaudeAPI", EnvironmentVariableTarget.User); | |
| } | |
| // If still no luck, try machine-wide environment variables | |
| if (string.IsNullOrEmpty(apiKey)) { | |
| apiKey = Environment.GetEnvironmentVariable("ClaudeAPI", EnvironmentVariableTarget.Machine); | |
| } | |
| // Ask user if they want to modify the prompts | |
| DialogResult customizePrompt = MessageBox.Show( | |
| "Do you want to modify the LLM prompt?", | |
| "Customize Prompt", | |
| MessageBoxButtons.YesNo, | |
| MessageBoxIcon.Question | |
| ); | |
| if (customizePrompt == DialogResult.Yes) | |
| { | |
| // Create form for prompt customization | |
| Form promptForm = new Form | |
| { | |
| Text = "Customize LLM Prompts", | |
| Size = new Size(800, 600), | |
| StartPosition = FormStartPosition.CenterScreen, | |
| FormBorderStyle = FormBorderStyle.FixedDialog, | |
| MaximizeBox = false, | |
| MinimizeBox = false | |
| }; | |
| // Add controls | |
| Label lblMeasurePrompt = new Label | |
| { | |
| Text = "Measure Description Prompt:", | |
| Location = new Point(20, 20), | |
| Size = new Size(750, 20) | |
| }; | |
| TextBox txtMeasurePrompt = new TextBox | |
| { | |
| Text = measurePrompt, | |
| Location = new Point(20, 45), | |
| Size = new Size(750, 200), | |
| Multiline = true, | |
| ScrollBars = ScrollBars.Vertical, | |
| AcceptsReturn = true, | |
| WordWrap = true | |
| }; | |
| Label lblColumnPrompt = new Label | |
| { | |
| Text = "Column Description Prompt:", | |
| Location = new Point(20, 255), | |
| Size = new Size(750, 20) | |
| }; | |
| TextBox txtColumnPrompt = new TextBox | |
| { | |
| Text = columnPrompt, | |
| Location = new Point(20, 280), | |
| Size = new Size(750, 200), | |
| Multiline = true, | |
| ScrollBars = ScrollBars.Vertical, | |
| AcceptsReturn = true, | |
| WordWrap = true | |
| }; | |
| // Note about placeholders | |
| Label lblPlaceholders = new Label | |
| { | |
| Text = "Note: Use {0} for object name, {1} for table name, {2} for data type (columns only)", | |
| Location = new Point(20, 490), | |
| Size = new Size(750, 20), | |
| Font = new Font(SystemFonts.DefaultFont.FontFamily, 8) | |
| }; | |
| // Buttons | |
| Button btnApply = new Button | |
| { | |
| Text = "Apply", | |
| DialogResult = DialogResult.OK, | |
| Location = new Point(170, 520), | |
| Size = new Size(150, 30) | |
| }; | |
| Button btnDefault = new Button | |
| { | |
| Text = "Nevermind (Use Default)", | |
| DialogResult = DialogResult.Ignore, | |
| Location = new Point(330, 520), | |
| Size = new Size(150, 30) | |
| }; | |
| Button btnCancel = new Button | |
| { | |
| Text = "Cancel", | |
| DialogResult = DialogResult.Cancel, | |
| Location = new Point(490, 520), | |
| Size = new Size(150, 30) | |
| }; | |
| // Add controls to form | |
| promptForm.Controls.Add(lblMeasurePrompt); | |
| promptForm.Controls.Add(txtMeasurePrompt); | |
| promptForm.Controls.Add(lblColumnPrompt); | |
| promptForm.Controls.Add(txtColumnPrompt); | |
| promptForm.Controls.Add(lblPlaceholders); | |
| promptForm.Controls.Add(btnApply); | |
| promptForm.Controls.Add(btnDefault); | |
| promptForm.Controls.Add(btnCancel); | |
| // Set cancel button | |
| promptForm.CancelButton = btnCancel; | |
| // Show dialog and get result | |
| DialogResult result = promptForm.ShowDialog(); | |
| // Handle user action | |
| switch (result) | |
| { | |
| case DialogResult.OK: | |
| // Use custom prompts | |
| measurePrompt = txtMeasurePrompt.Text; | |
| columnPrompt = txtColumnPrompt.Text; | |
| break; | |
| case DialogResult.Ignore: | |
| // Use default prompts - no action needed | |
| break; | |
| case DialogResult.Cancel: | |
| // Cancel the operation | |
| return; | |
| } | |
| } | |
| // Create a combined list of selected objects (measures and columns) | |
| var selectedObjects = new List<object>(); | |
| selectedObjects.AddRange(Selected.Measures.Cast<object>()); | |
| selectedObjects.AddRange(Selected.Columns.Cast<object>()); | |
| // Verify something is selected | |
| if (selectedObjects.Count == 0) | |
| { | |
| if (DialogResult.Cancel == MessageBox.Show( | |
| "Please select at least one measure or column.", | |
| "No Objects Selected", | |
| MessageBoxButtons.OKCancel, | |
| MessageBoxIcon.Information)) | |
| { | |
| return; | |
| } | |
| return; | |
| } | |
| int measuresCount = Selected.Measures.Count(); | |
| int columnsCount = Selected.Columns.Count(); | |
| int totalCount = selectedObjects.Count; | |
| int currentIndex = 0; | |
| int describedMeasures = 0; | |
| int describedColumns = 0; | |
| // Show initial count with cancel option | |
| if (DialogResult.Cancel == MessageBox.Show( | |
| $"Processing {totalCount} selected objects ({measuresCount} measures, {columnsCount} columns).", | |
| "Script Info", | |
| MessageBoxButtons.OKCancel, | |
| MessageBoxIcon.Information)) | |
| { | |
| return; | |
| } | |
| // Set up HTTP client | |
| using (HttpClient client = new HttpClient()) | |
| { | |
| client.DefaultRequestHeaders.Add("x-api-key", apiKey); | |
| client.DefaultRequestHeaders.Add("anthropic-version", "2023-06-01"); | |
| // Process all selected objects | |
| foreach (var obj in selectedObjects) | |
| { | |
| currentIndex++; | |
| bool isMeasure = obj is Measure; | |
| string objType = isMeasure ? "measure" : "column"; | |
| string objName = isMeasure ? (obj as Measure).Name : (obj as Column).Name; | |
| string tableName = isMeasure ? (obj as Measure).Table.Name : (obj as Column).Table.Name; | |
| string currentDescription = isMeasure ? | |
| (obj as Measure).Description : | |
| (obj as Column).Description; | |
| // Skip if don't overwrite is enabled and description exists | |
| if (dontOverwrite && !string.IsNullOrEmpty(currentDescription)) | |
| { | |
| Info($"Skipping {objName} - description already exists."); | |
| continue; | |
| } | |
| // Show progress | |
| string progressBar = GetProgressBar(currentIndex, totalCount, 20); | |
| Info($"Processing {objType} {currentIndex} of {totalCount}: [{tableName}].[{objName}] {progressBar}"); | |
| try | |
| { | |
| // Get sample data for context | |
| string sampleData = GetSampleData(obj, isMeasure); | |
| // Generate description suggestion | |
| string description = GetDescriptionSuggestion(client, obj, isMeasure, objName, tableName, sampleData); | |
| if (string.IsNullOrEmpty(description)) | |
| { | |
| Info($"Skipping {objName} - no description suggestion received."); | |
| continue; | |
| } | |
| // Show UI dialog for user to confirm or modify | |
| DialogResult result = ShowDescriptionDialog( | |
| obj, | |
| isMeasure, | |
| objName, | |
| tableName, | |
| description, | |
| sampleData, | |
| out string finalDescription | |
| ); | |
| // Handle user action | |
| switch (result) | |
| { | |
| case DialogResult.OK: | |
| ApplyDescription(obj, isMeasure, finalDescription); | |
| if (isMeasure) describedMeasures++; | |
| else describedColumns++; | |
| break; | |
| case DialogResult.Retry: | |
| // Try again with the same object | |
| currentIndex--; | |
| continue; | |
| case DialogResult.Ignore: | |
| // Skip to next | |
| Info($"Skipped {objName}"); | |
| break; | |
| case DialogResult.Cancel: | |
| // User canceled the entire operation | |
| Info("Operation canceled by user."); | |
| return; | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Error($"Error processing {objName}: {ex.Message}"); | |
| } | |
| } | |
| // Show completion message | |
| MessageBox.Show( | |
| $"Completed! Added descriptions to {describedMeasures} measures and {describedColumns} columns.", | |
| "Process Complete", | |
| MessageBoxButtons.OK, | |
| MessageBoxIcon.Information | |
| ); | |
| } | |
| } | |
| // Gets sample data for the object | |
| string GetSampleData(object obj, bool isMeasure) | |
| { | |
| try | |
| { | |
| if (isMeasure) | |
| { | |
| var measure = obj as Measure; | |
| // Simple evaluation of the measure | |
| var result = EvaluateDax(measure.DaxObjectFullName); | |
| return $"Sample value: {result.ToString()}"; | |
| } | |
| else | |
| { | |
| var column = obj as Column; | |
| // Get top 10 distinct values from the column as a comma-delimited list | |
| string dax = $"CONCATENATEX(TOPN(10, DISTINCT({column.DaxObjectFullName})), {column.DaxObjectFullName}, \", \")"; | |
| var result = EvaluateDax(dax); | |
| return $"Sample values: {result.ToString()}"; | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| // Show error dialog with Cancel option | |
| DialogResult result = MessageBox.Show( | |
| $"Error getting sample data for {(isMeasure ? (obj as Measure).Name : (obj as Column).Name)}: {ex.Message}\n\nDo you want to continue without sample data?", | |
| "Error", | |
| MessageBoxButtons.OKCancel, | |
| MessageBoxIcon.Warning | |
| ); | |
| if (result == DialogResult.Cancel) | |
| { | |
| throw new Exception("Operation canceled by user"); | |
| } | |
| return $"Unable to get sample data: {ex.Message}"; | |
| } | |
| } | |
| // Gets description suggestion from Claude | |
| string GetDescriptionSuggestion(HttpClient client, object obj, bool isMeasure, string objName, string tableName, string sampleData) | |
| { | |
| string prompt; | |
| string context; | |
| if (isMeasure) | |
| { | |
| var measure = obj as Measure; | |
| prompt = string.Format(measurePrompt, objName, tableName); | |
| context = $"Name: {objName}\nTable: {tableName}\nDAX Formula: {measure.Expression}\n" + | |
| $"Current Description: {measure.Description}\n" + | |
| $"Sample Data: {sampleData}"; | |
| } | |
| else | |
| { | |
| var column = obj as Column; | |
| prompt = string.Format(columnPrompt, objName, tableName, column.DataType); | |
| context = $"Name: {objName}\nTable: {tableName}\nData Type: {column.DataType}\n" + | |
| $"Current Description: {column.Description}\n" + | |
| $"Sample Data: {sampleData}"; | |
| } | |
| // Create API request body | |
| var requestBody = new | |
| { | |
| model = claudeModel, | |
| max_tokens = maxTokens, | |
| messages = new[] | |
| { | |
| new { role = "user", content = prompt + "\n\n" + context } | |
| } | |
| }; | |
| string jsonBody = JsonConvert.SerializeObject(requestBody); | |
| var content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); | |
| // Call Claude API - using .Result instead of await | |
| var response = client.PostAsync(apiUrl, content).Result; | |
| if (response.IsSuccessStatusCode) | |
| { | |
| var jsonResponse = JObject.Parse(response.Content.ReadAsStringAsync().Result); | |
| string description = jsonResponse["content"][0]["text"].ToString().Trim(); | |
| // Normalize line endings to Environment.NewLine for consistent display | |
| description = description.Replace("\r\n", "\n").Replace("\n", Environment.NewLine); | |
| return description; | |
| } | |
| else | |
| { | |
| Error($"API error for {objName}: {response.StatusCode} - {response.ReasonPhrase}"); | |
| return string.Empty; | |
| } | |
| } | |
| // Creates a dialog for the user to modify the suggested description | |
| DialogResult ShowDescriptionDialog(object obj, bool isMeasure, string objName, string tableName, string suggestion, string sampleData, out string finalDescription) | |
| { | |
| // Initialize output parameter | |
| finalDescription = suggestion; | |
| // Create form | |
| Form form = new Form | |
| { | |
| Text = $"Description for {(isMeasure ? "Measure" : "Column")}: {objName}", | |
| Size = new Size(650, 500), | |
| StartPosition = FormStartPosition.CenterScreen, | |
| FormBorderStyle = FormBorderStyle.FixedDialog, | |
| MaximizeBox = false, | |
| MinimizeBox = false | |
| }; | |
| // Add controls | |
| Label lblInfo = new Label | |
| { | |
| Text = $"{(isMeasure ? "Measure" : "Column")}: [{tableName}].[{objName}]", | |
| Location = new Point(20, 20), | |
| Size = new Size(600, 20), | |
| Font = new Font(SystemFonts.DefaultFont.FontFamily, 10, FontStyle.Bold) | |
| }; | |
| // Context section | |
| GroupBox gbContext = new GroupBox | |
| { | |
| Text = "Context", | |
| Location = new Point(20, 50), | |
| Size = new Size(600, 140) | |
| }; | |
| Label lblExpression = new Label | |
| { | |
| Text = isMeasure ? $"Expression: {(obj as Measure).Expression}" : $"Data Type: {(obj as Column).DataType}", | |
| Location = new Point(10, 20), | |
| Size = new Size(580, 40), | |
| AutoSize = false | |
| }; | |
| Label lblSampleData = new Label | |
| { | |
| Text = "Sample Data:", | |
| Location = new Point(10, 65), | |
| Size = new Size(100, 20) | |
| }; | |
| TextBox txtSampleData = new TextBox | |
| { | |
| Text = sampleData, | |
| Location = new Point(10, 85), | |
| Size = new Size(580, 45), | |
| Multiline = true, | |
| ReadOnly = true, | |
| ScrollBars = ScrollBars.Vertical | |
| }; | |
| gbContext.Controls.Add(lblExpression); | |
| gbContext.Controls.Add(lblSampleData); | |
| gbContext.Controls.Add(txtSampleData); | |
| // Description section | |
| Label lblDescription = new Label | |
| { | |
| Text = "Description:", | |
| Location = new Point(20, 200), | |
| Size = new Size(150, 20), | |
| Font = new Font(SystemFonts.DefaultFont.FontFamily, 9, FontStyle.Bold) | |
| }; | |
| TextBox txtDescription = new TextBox | |
| { | |
| Text = suggestion, | |
| Location = new Point(20, 225), | |
| Size = new Size(600, 150), | |
| Multiline = true, | |
| ScrollBars = ScrollBars.Vertical, | |
| AcceptsReturn = true, | |
| WordWrap = true | |
| }; | |
| // Buttons | |
| Button btnAccept = new Button | |
| { | |
| Text = "Accept", | |
| DialogResult = DialogResult.OK, | |
| Location = new Point(20, 390), | |
| Size = new Size(130, 35) | |
| }; | |
| Button btnTryAgain = new Button | |
| { | |
| Text = "Try Again", | |
| DialogResult = DialogResult.Retry, | |
| Location = new Point(175, 390), | |
| Size = new Size(130, 35) | |
| }; | |
| Button btnNext = new Button | |
| { | |
| Text = "Next", | |
| DialogResult = DialogResult.Ignore, | |
| Location = new Point(330, 390), | |
| Size = new Size(130, 35) | |
| }; | |
| Button btnCancel = new Button | |
| { | |
| Text = "Cancel", | |
| DialogResult = DialogResult.Cancel, | |
| Location = new Point(485, 390), | |
| Size = new Size(130, 35) | |
| }; | |
| // Add controls to form | |
| form.Controls.Add(lblInfo); | |
| form.Controls.Add(gbContext); | |
| form.Controls.Add(lblDescription); | |
| form.Controls.Add(txtDescription); | |
| form.Controls.Add(btnAccept); | |
| form.Controls.Add(btnTryAgain); | |
| form.Controls.Add(btnNext); | |
| form.Controls.Add(btnCancel); | |
| // Don't set auto-accept button to allow Enter for newlines | |
| form.CancelButton = btnCancel; | |
| // Handle Ctrl+Enter for accept | |
| txtDescription.KeyDown += (sender, e) => { | |
| if (e.Control && e.KeyCode == Keys.Enter) | |
| { | |
| e.SuppressKeyPress = true; | |
| btnAccept.PerformClick(); | |
| } | |
| }; | |
| // Add info label about keyboard shortcuts | |
| Label lblShortcuts = new Label | |
| { | |
| Text = "Shortcuts: Ctrl+Enter to Accept, Enter for new line", | |
| Location = new Point(20, 370), | |
| Size = new Size(300, 15), | |
| Font = new Font(SystemFonts.DefaultFont.FontFamily, 8) | |
| }; | |
| form.Controls.Add(lblShortcuts); | |
| // Show dialog and get result | |
| DialogResult result = form.ShowDialog(); | |
| // Update final description if user clicked OK | |
| if (result == DialogResult.OK) | |
| { | |
| finalDescription = txtDescription.Text; | |
| } | |
| return result; | |
| } | |
| // Applies description to the object | |
| void ApplyDescription(object obj, bool isMeasure, string description) | |
| { | |
| if (isMeasure) | |
| { | |
| var measure = obj as Measure; | |
| measure.Description = description; | |
| } | |
| else | |
| { | |
| var column = obj as Column; | |
| column.Description = description; | |
| } | |
| } | |
| // Helper function to create a text-based progress bar | |
| string GetProgressBar(int current, int total, int barLength) | |
| { | |
| int filledLength = (int)Math.Round((double)current / total * barLength); | |
| return $"[{new string('#', filledLength)}{new string('-', barLength - filledLength)}]"; | |
| } | |
| // Call the main method to run the script | |
| Main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment