diff --git a/MarkMpn.Sql4Cds.XTB/CreateCopilotAssistantForm.cs b/MarkMpn.Sql4Cds.XTB/CreateCopilotAssistantForm.cs index cbbbcccb..55762755 100644 --- a/MarkMpn.Sql4Cds.XTB/CreateCopilotAssistantForm.cs +++ b/MarkMpn.Sql4Cds.XTB/CreateCopilotAssistantForm.cs @@ -50,91 +50,98 @@ private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerComple private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { - string instructions; - - using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MarkMpn.Sql4Cds.XTB.Resources.CopilotInstructions.txt")) - using (var reader = new StreamReader(stream)) - { - instructions = reader.ReadToEnd(); - } - - var client = _endpoint == "https://api.openai.com/" ? new OpenAI.OpenAIClient(new System.ClientModel.ApiKeyCredential(_apiKey)) : new Azure.AI.OpenAI.AzureOpenAIClient(new Uri(_endpoint), new Azure.AzureKeyCredential(_apiKey)); + var client = String.IsNullOrEmpty(_endpoint) ? new OpenAI.OpenAIClient(new System.ClientModel.ApiKeyCredential(_apiKey)) : new Azure.AI.OpenAI.AzureOpenAIClient(new Uri(_endpoint), new Azure.AzureKeyCredential(_apiKey)); #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. var assistantClient = client.GetAssistantClient(); #pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - var assistant = new AssistantCreationOptions + + Assistant created = assistantClient.CreateAssistant(modelNameTextBox.Text, Definition); + e.Result = created.Id; + } + + public static AssistantCreationOptions Definition + { + get { - Name = "SQL 4 CDS Copilot", - Description = "An AI assistant to help you write SQL queries in SQL 4 CDS", - Instructions = instructions, - Tools = + string instructions; + + using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("MarkMpn.Sql4Cds.XTB.Resources.CopilotInstructions.txt")) + using (var reader = new StreamReader(stream)) { - new FunctionToolDefinition - { - FunctionName = "list_tables", - Description = "Get the list of tables in the current environment", - Parameters = BinaryData.FromObjectAsJson(new { type = "object", properties = new { } }) - }, - new FunctionToolDefinition - { - FunctionName = "get_columns_in_table", - Description = "Get the list of columns in a table, including the available values for optionset columns and the relationships for foreign key columns", - Parameters = BinaryData.FromObjectAsJson(new { - type = "object", - properties = new { - table_name = new { - type = "string", - description = "The name of the table, e.g. 'account'" - } - }, - required = new [] { "table_name" } - }) - }, - new FunctionToolDefinition - { - FunctionName = "get_current_query", - Description = "Gets the contents of the query the user is currently editing", - Parameters = BinaryData.FromObjectAsJson(new { type = "object", properties = new { } }) - }, - new FunctionToolDefinition - { - FunctionName = "execute_query", - Description = "Runs a SQL query and returns the result. Only queries which have previously been shown to the user will be run", - Parameters = BinaryData.FromObjectAsJson(new { - type = "object", - properties = new { - query = new { - type = "string", - description = "The SQL query to execute" - } - }, - required = new [] { "query" } - }) - }, - new FunctionToolDefinition + instructions = reader.ReadToEnd(); + } + + return new AssistantCreationOptions + { + Name = "SQL 4 CDS Copilot", + Description = "An AI assistant to help you write SQL queries in SQL 4 CDS", + Instructions = instructions, + Tools = { - FunctionName = "find_relationship", - Description = "Finds the path of joins that can be used to connect two tables", - Parameters = BinaryData.FromObjectAsJson(new { - type = "object", - properties = new { - table1 = new { - type = "string", - description = "The table to join from" + new FunctionToolDefinition + { + FunctionName = "list_tables", + Description = "Get the list of tables in the current environment", + Parameters = BinaryData.FromObjectAsJson(new { type = "object", properties = new { } }) + }, + new FunctionToolDefinition + { + FunctionName = "get_columns_in_table", + Description = "Get the list of columns in a table, including the available values for optionset columns and the relationships for foreign key columns", + Parameters = BinaryData.FromObjectAsJson(new { + type = "object", + properties = new { + table_name = new { + type = "string", + description = "The name of the table, e.g. 'account'" + } + }, + required = new [] { "table_name" } + }) + }, + new FunctionToolDefinition + { + FunctionName = "get_current_query", + Description = "Gets the contents of the query the user is currently editing", + Parameters = BinaryData.FromObjectAsJson(new { type = "object", properties = new { } }) + }, + new FunctionToolDefinition + { + FunctionName = "execute_query", + Description = "Runs a SQL query and returns the result. Only queries which have previously been shown to the user will be run", + Parameters = BinaryData.FromObjectAsJson(new { + type = "object", + properties = new { + query = new { + type = "string", + description = "The SQL query to execute" + } }, - table2 = new { - type = "string", - description = "The table to join to" - } - }, - required = new [] { "table1", "table2" } - }) + required = new [] { "query" } + }) + }, + new FunctionToolDefinition + { + FunctionName = "find_relationship", + Description = "Finds the path of joins that can be used to connect two tables", + Parameters = BinaryData.FromObjectAsJson(new { + type = "object", + properties = new { + table1 = new { + type = "string", + description = "The table to join from" + }, + table2 = new { + type = "string", + description = "The table to join to" + } + }, + required = new [] { "table1", "table2" } + }) + } } - } - }; - - Assistant created = assistantClient.CreateAssistant(modelNameTextBox.Text, assistant); - e.Result = created.Id; + }; + } } } } diff --git a/MarkMpn.Sql4Cds.XTB/Settings.cs b/MarkMpn.Sql4Cds.XTB/Settings.cs index ca52cf71..fef9d5b7 100644 --- a/MarkMpn.Sql4Cds.XTB/Settings.cs +++ b/MarkMpn.Sql4Cds.XTB/Settings.cs @@ -78,6 +78,8 @@ public class Settings public string AssistantID { get; set; } public bool AllowCopilotSelectQueries { get; set; } + + public string AssistantVersion { get; set; } } public class TabContent diff --git a/MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs b/MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs index b45d0ebc..0e596541 100644 --- a/MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs +++ b/MarkMpn.Sql4Cds.XTB/SettingsForm.Designer.cs @@ -981,7 +981,7 @@ private void InitializeComponent() this.label21.Name = "label21"; this.label21.Size = new System.Drawing.Size(91, 13); this.label21.TabIndex = 0; - this.label21.Text = "Open AI Endpoint"; + this.label21.Text = "Azure Open AI Endpoint"; // // label25 // @@ -989,7 +989,7 @@ private void InitializeComponent() this.label25.Name = "label25"; this.label25.Size = new System.Drawing.Size(399, 41); this.label25.TabIndex = 9; - this.label25.Text = "If using OpenAI, enter https://api.openai.com/v1\r\nFor Azure OpenAI, enter the end" + + this.label25.Text = "Leave blank when using OpenAI.\r\nFor Azure OpenAI, enter the end" + "point of your resource, e.g. https://my-copilot.openai.azure.com/"; // // SettingsForm diff --git a/MarkMpn.Sql4Cds.XTB/SettingsForm.cs b/MarkMpn.Sql4Cds.XTB/SettingsForm.cs index 7af8b728..422767ff 100644 --- a/MarkMpn.Sql4Cds.XTB/SettingsForm.cs +++ b/MarkMpn.Sql4Cds.XTB/SettingsForm.cs @@ -4,6 +4,7 @@ using System.Data; using System.Drawing; using System.Linq; +using System.Reflection; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; @@ -17,6 +18,7 @@ public partial class SettingsForm : Form private readonly Settings _settings; private readonly FetchXml2SqlOptions _fetchXml2SqlOptions; private readonly PluginControl _pluginControl; + private string _assistantVersion; public SettingsForm(Settings settings, PluginControl plugin) { @@ -139,6 +141,9 @@ protected override void OnClosing(CancelEventArgs e) _settings.OpenAIKey = openAiKeyTextBox.Text; _settings.AssistantID = assistantIdTextBox.Text; _settings.AllowCopilotSelectQueries = allowCopilotSelectQueriesCheckBox.Checked; + + if (_assistantVersion != null) + _settings.AssistantVersion = _assistantVersion; } } @@ -222,6 +227,7 @@ private void createAssistantbutton_Click(object sender, EventArgs e) if (form.ShowDialog(this) == DialogResult.OK) { assistantIdTextBox.Text = form.AssistantId; + _assistantVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); } } } diff --git a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs index 7cfeb5d9..2c0d3f23 100644 --- a/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs +++ b/MarkMpn.Sql4Cds.XTB/SqlQueryControl.cs @@ -45,6 +45,7 @@ using QuikGraph.Algorithms; using ScintillaNET; using xrmtb.XrmToolBox.Controls.Controls; +using XrmToolBox.Extensibility; #pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. @@ -195,7 +196,7 @@ public SqlQueryControl(ConnectionDetail con, IDictionary dat Icon = _sqlIcon; // Show/hide the copilot panel - copilotSplitContainer.Panel2Collapsed = String.IsNullOrEmpty(Settings.Instance.OpenAIEndpoint) || String.IsNullOrEmpty(Settings.Instance.OpenAIKey) || String.IsNullOrEmpty(Settings.Instance.AssistantID); + copilotSplitContainer.Panel2Collapsed = String.IsNullOrEmpty(Settings.Instance.OpenAIKey) || String.IsNullOrEmpty(Settings.Instance.AssistantID); Connect(); @@ -237,7 +238,7 @@ public override void SettingsChanged() _autocomplete.Font = new Font(Settings.Instance.EditorFontName, Settings.Instance.EditorFontSize); // Show/hide the copilot panel - copilotSplitContainer.Panel2Collapsed = String.IsNullOrEmpty(Settings.Instance.OpenAIEndpoint) || String.IsNullOrEmpty(Settings.Instance.OpenAIKey) || String.IsNullOrEmpty(Settings.Instance.AssistantID); + copilotSplitContainer.Panel2Collapsed = String.IsNullOrEmpty(Settings.Instance.OpenAIKey) || String.IsNullOrEmpty(Settings.Instance.AssistantID); } protected override void OnClosing(CancelEventArgs e) @@ -2096,7 +2097,8 @@ private async Task InitCopilot() copilotSplitContainer.Panel2.Controls.Add(_copilotWebView); await _copilotWebView.EnsureCoreWebView2Async(); _copilotWebView.Source = new Uri(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Copilot.html")); - _copilotWebView.CoreWebView2.AddHostObjectToScript("sql4cds", new CopilotScriptObject(this, _copilotWebView)); + var script = new CopilotScriptObject(this, _copilotWebView); + _copilotWebView.CoreWebView2.AddHostObjectToScript("sql4cds", script); _copilotWebView.Focus(); } @@ -2167,8 +2169,23 @@ public async Task SendMessage(string request) { if (_assistantClient == null) { - var client = Settings.Instance.OpenAIEndpoint == "https://api.openai.com/" ? new OpenAI.OpenAIClient(new ApiKeyCredential(Settings.Instance.OpenAIKey)) : new Azure.AI.OpenAI.AzureOpenAIClient(new Uri(Settings.Instance.OpenAIEndpoint), new Azure.AzureKeyCredential(Settings.Instance.OpenAIKey)); + var client = String.IsNullOrEmpty(Settings.Instance.OpenAIEndpoint) ? new OpenAI.OpenAIClient(new ApiKeyCredential(Settings.Instance.OpenAIKey)) : new Azure.AI.OpenAI.AzureOpenAIClient(new Uri(Settings.Instance.OpenAIEndpoint), new Azure.AzureKeyCredential(Settings.Instance.OpenAIKey)); _assistantClient = client.GetAssistantClient(); + + if (!Version.TryParse(Settings.Instance.AssistantVersion, out var assistantVersion) || assistantVersion < Assembly.GetExecutingAssembly().GetName().Version) + { + // Update the assistant definition before we try to use it + var definition = CreateCopilotAssistantForm.Definition; + + await _assistantClient.ModifyAssistantAsync(Settings.Instance.AssistantID, new AssistantModificationOptions + { + Instructions = definition.Instructions, + DefaultTools = definition.Tools, + }); + + Settings.Instance.AssistantVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString(); + SettingsManager.Instance.Save(typeof(PluginControl), Settings.Instance); + } } if (_assistant == null)