Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save data-goblin/f03808e3353b2851cf5ef5760bf9c71d to your computer and use it in GitHub Desktop.

Select an option

Save data-goblin/f03808e3353b2851cf5ef5760bf9c71d to your computer and use it in GitHub Desktop.
#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