diff --git a/src/Cellm/AddIn/ArgumentParser.cs b/src/Cellm/AddIn/ArgumentParser.cs deleted file mode 100644 index e9d5e2f..0000000 --- a/src/Cellm/AddIn/ArgumentParser.cs +++ /dev/null @@ -1,277 +0,0 @@ -using System.Text; -using Cellm.AddIn.Exceptions; -using Cellm.Services.Configuration; -using ExcelDna.Integration; -using Microsoft.Extensions.Configuration; -using Microsoft.Office.Interop.Excel; - -namespace Cellm.AddIn; - -public record Arguments(string Provider, string Model, string Context, string Instructions, double Temperature); - -public class ArgumentParser -{ - private string? _provider; - private string? _model; - private string? _context; - private string? _instructions; - private double? _temperature; - - private readonly IConfiguration _configuration; - - public ArgumentParser(IConfiguration configuration) - { - _configuration = configuration; - } - - public ArgumentParser AddProvider(object providerAndModel) - { - switch (providerAndModel) - { - case string providerAndModelAsString: - _provider = GetProvider(providerAndModelAsString); - break; - case ExcelReference providerAndModelAsExcelReference: - if (providerAndModelAsExcelReference.RowFirst != providerAndModelAsExcelReference.RowLast || - providerAndModelAsExcelReference.ColumnFirst != providerAndModelAsExcelReference.ColumnLast) - { - throw new ArgumentException("Provider argument must be a single cell"); - } - - var providerAndModelToString = providerAndModelAsExcelReference.GetValue()?.ToString() - ?? throw new ArgumentException("Provider argument must be a valid cell reference"); - _model = GetModel(providerAndModelToString); - break; - default: - throw new ArgumentException("Provider argument must be a cell or a string"); - } - - return this; - } - - public ArgumentParser AddModel(object providerAndModel) - { - switch (providerAndModel) - { - case string providerAndModelAsString: - _model = GetModel(providerAndModelAsString); - break; - case ExcelReference providerAndModelAsExcelReference: - if (providerAndModelAsExcelReference.RowFirst != providerAndModelAsExcelReference.RowLast || - providerAndModelAsExcelReference.ColumnFirst != providerAndModelAsExcelReference.ColumnLast) - { - throw new ArgumentException("Model argument argument must be a single cell"); - } - - var providerAndModelToString = providerAndModelAsExcelReference.GetValue()?.ToString() ?? throw new ArgumentException("Model argument must be a valid cell reference"); - _model = GetModel(providerAndModelToString); - break; - default: - throw new ArgumentException("Model argument must be a cell or a string"); - } - - return this; - } - - public ArgumentParser AddContext(object context) - { - if (context is ExcelReference contextAsExcelReference) - { - _context = FormatCells(contextAsExcelReference); - } - else - { - throw new ArgumentException("Context argument must be a cell or a range of cells", nameof(context)); - } - - return this; - } - - public ArgumentParser AddInstructionsOrTemperature(object instructionsOrTemperature) - { - if (instructionsOrTemperature is ExcelMissing) - { - return this; - } - - switch (instructionsOrTemperature) - { - case string instructionsOrTemperatureAsString: - _instructions = instructionsOrTemperatureAsString; - break; - case ExcelReference instructionsOrTemperatureAsExcelReference: - _instructions = FormatCells(instructionsOrTemperatureAsExcelReference); - break; - case double instructionsOrTemperatureAsDouble: - AddTemperature(instructionsOrTemperatureAsDouble); - break; - default: - throw new ArgumentException("InstructionsOrTemperature argument must be a cell, a range of cells, or a string (instructions) or a double (temperature)."); - } - - return this; - } - - public ArgumentParser AddTemperature(object temperature) - { - if (temperature is ExcelMissing) - { - return this; - } - - if (temperature is double temperatureAsDouble) - { - if (temperatureAsDouble < 0 || temperatureAsDouble > 1) - { - throw new ArgumentOutOfRangeException(nameof(temperature), "Temperature argument must be between 0 and 1"); - } - - _temperature = temperatureAsDouble; - } - else - { - throw new ArgumentException("Temperature argument must be a double (temperature).", nameof(temperature)); - } - - return this; - } - - public Arguments Parse() - { - var provider = _configuration.GetSection(nameof(CellmConfiguration)).GetValue(nameof(CellmConfiguration.DefaultProvider)) - ?? throw new ArgumentException(nameof(CellmConfiguration.DefaultProvider)); - - if (!string.IsNullOrEmpty(_provider)) - { - provider = _provider; - } - - var model = _configuration.GetSection($"{provider}Configuration").GetValue(nameof(IProviderConfiguration.DefaultModel)) - ?? throw new ArgumentException(nameof(IProviderConfiguration.DefaultModel)); - - if (!string.IsNullOrEmpty(_model)) - { - model = _model; - } - - if (_context is null) - { - throw new InvalidOperationException("Context argument is required"); - } - - // Parse cells - var contextBuilder = new StringBuilder(); - contextBuilder.AppendLine(""); - contextBuilder.Append(_context); - contextBuilder.AppendLine(""); - - // Parse instructions - var instructionsBuilder = new StringBuilder(); - instructionsBuilder.AppendLine(""); - - if (string.IsNullOrEmpty(_instructions)) - { - instructionsBuilder.AppendLine(SystemMessages.InlineInstructions); - } - else - { - instructionsBuilder.AppendLine(_instructions); - } - - instructionsBuilder.AppendLine(""); - - var temperature = _configuration.GetSection(nameof(CellmConfiguration)).GetValue(nameof(CellmConfiguration.DefaultTemperature)); - - if (_temperature is not null) - { - temperature = Convert.ToDouble(_temperature); - } - - return new Arguments(provider, model, contextBuilder.ToString(), instructionsBuilder.ToString(), temperature); - } - - private static string GetProvider(string providerAndModel) - { - var index = providerAndModel.IndexOf("/"); - - if (index < 0) - { - throw new ArgumentException($"Provider and model argument must on the form \"Provider/Model\""); - } - - return providerAndModel[..index]; - } - - private static string GetModel(string providerAndModel) - { - var index = providerAndModel.IndexOf("/"); - - if (index < 0) - { - throw new ArgumentException($"Provider and model argument must on the form \"Provider/Model\""); - } - - return providerAndModel[(index + 1)..]; - } - - private static string FormatCells(ExcelReference reference) - { - try - { - var app = (Application)ExcelDnaUtil.Application; - var sheetName = (string)XlCall.Excel(XlCall.xlSheetNm, reference); - sheetName = sheetName[(sheetName.LastIndexOf("]") + 1)..]; - var worksheet = app.Sheets[sheetName]; - - var tableBuilder = new StringBuilder(); - var valueBuilder = new StringBuilder(); - - var rows = reference.RowLast - reference.RowFirst + 1; - var columns = reference.ColumnLast - reference.ColumnFirst + 1; - - for (int row = 0; row < rows; row++) - { - for (int column = 0; column < columns; column++) - { - var value = worksheet.Cells[reference.RowFirst + row + 1, reference.ColumnFirst + column + 1].Text; - valueBuilder.Append(value); - - tableBuilder.Append("| "); - tableBuilder.Append(GetColumnName(reference.ColumnFirst + column) + GetRowName(reference.RowFirst + row)); - tableBuilder.Append(' '); - tableBuilder.Append(value); - tableBuilder.Append(' '); - } - - tableBuilder.AppendLine("|"); - } - - if (string.IsNullOrEmpty(valueBuilder.ToString())) - { - throw new ArgumentException("Context cannot not be empty"); - } - - return tableBuilder.ToString(); - } - catch (Exception ex) - { - throw new CellmException("Failed to format context: ", ex); - } - } - - private static string GetColumnName(int columnNumber) - { - string columnName = ""; - while (columnNumber >= 0) - { - columnName = (char)('A' + columnNumber % 26) + columnName; - columnNumber = columnNumber / 26 - 1; - } - return columnName; - } - - private static string GetRowName(int rowNumber) - { - return (rowNumber + 1).ToString(); - } -} \ No newline at end of file diff --git a/src/Cellm/AddIn/Functions.cs b/src/Cellm/AddIn/Functions.cs index 9426368..30e2dd3 100644 --- a/src/Cellm/AddIn/Functions.cs +++ b/src/Cellm/AddIn/Functions.cs @@ -29,7 +29,7 @@ public static class Functions /// [ExcelFunction(Name = "PROMPT", Description = "Send a prompt to the default model")] public static object Prompt( - [ExcelArgument(AllowReference = true, Name = "Context", Description = "A cell or range of cells")] object context, + [ExcelArgument(AllowReference = true, Name = "InstructionsOrContext", Description = "A string with instructions or a cell or range of cells with context")] object context, [ExcelArgument(Name = "InstructionsOrTemperature", Description = "A cell or range of cells with instructions or a temperature")] object instructionsOrTemperature, [ExcelArgument(Name = "Temperature", Description = "Temperature")] object temperature) { @@ -52,7 +52,7 @@ public static object Prompt( /// Sends a prompt to the specified model. /// /// The provider and model in the format "provider/model". - /// A cell or range of cells containing the context for the prompt. + /// A string with instructions or a cell or range of cells with context. /// /// A cell or range of cells with instructions, or a temperature value. /// If omitted, any instructions found in the context will be used. @@ -67,16 +67,16 @@ public static object Prompt( [ExcelFunction(Name = "PROMPTWITH", Description = "Send a prompt to a specific model")] public static object PromptWith( [ExcelArgument(AllowReference = true, Name = "Provider/Model")] object providerAndModel, - [ExcelArgument(AllowReference = true, Name = "Context", Description = "A cell or range of cells")] object context, + [ExcelArgument(AllowReference = true, Name = "InstructionsOrContext", Description = "A string with instructions or a cell or range of cells with context")] object instructionsOrContext, [ExcelArgument(Name = "InstructionsOrTemperature", Description = "A cell or range of cells with instructions or a temperature")] object instructionsOrTemperature, [ExcelArgument(Name = "Temperature", Description = "Temperature")] object temperature) { try { - var arguments = ServiceLocator.Get() + var arguments = ServiceLocator.Get() .AddProvider(providerAndModel) .AddModel(providerAndModel) - .AddContext(context) + .AddInstructionsOrContext(instructionsOrContext) .AddInstructionsOrTemperature(instructionsOrTemperature) .AddTemperature(temperature) .Parse(); @@ -94,7 +94,7 @@ public static object PromptWith( .Build(); // ExcelAsyncUtil yields Excel's main thread, Task.Run enables async/await in inner code - return ExcelAsyncUtil.Run(nameof(PromptWith), new object[] { providerAndModel, context, instructionsOrTemperature, temperature }, () => + return ExcelAsyncUtil.Run(nameof(PromptWith), new object[] { providerAndModel, instructionsOrContext, instructionsOrTemperature, temperature }, () => { return Task.Run(async () => await CallModelAsync(prompt, arguments.Provider)).GetAwaiter().GetResult(); }); diff --git a/src/Cellm/AddIn/PromptWithArgumentParser.cs b/src/Cellm/AddIn/PromptWithArgumentParser.cs new file mode 100644 index 0000000..8704892 --- /dev/null +++ b/src/Cellm/AddIn/PromptWithArgumentParser.cs @@ -0,0 +1,233 @@ +using System.Text; +using Cellm.AddIn.Exceptions; +using ExcelDna.Integration; +using Microsoft.Extensions.Configuration; +using Microsoft.Office.Interop.Excel; + +namespace Cellm.AddIn; + +public record Arguments(string Provider, string Model, string Context, string Instructions, double Temperature); + +public class PromptWithArgumentParser +{ + private string? _provider; + private string? _model; + private object? _instructionsOrContext; + private object? _instructionsOrTemperature; + private object? _temperature; + + private readonly IConfiguration _configuration; + + public PromptWithArgumentParser(IConfiguration configuration) + { + _configuration = configuration; + } + + public PromptWithArgumentParser AddProvider(object providerAndModel) + { + _provider = providerAndModel switch + { + string providerAndModelAsString => GetProvider(providerAndModelAsString), + ExcelReference providerAndModelAsExcelReference => GetProvider(GetCellAsString(providerAndModelAsExcelReference)), + _ => throw new ArgumentException("Provider and model argument must be a string or single cell") + }; + + return this; + } + + public PromptWithArgumentParser AddModel(object providerAndModel) + { + _model = providerAndModel switch + { + string providerAndModelAsString => GetModel(providerAndModelAsString), + ExcelReference providerAndModelAsExcelReference => GetModel(GetCellAsString(providerAndModelAsExcelReference)), + _ => throw new ArgumentException("Provider and model argument must be a string or single cell") + }; + + return this; + } + + public PromptWithArgumentParser AddInstructionsOrContext(object instructionsOrContext) + { + _instructionsOrContext = instructionsOrContext; + + return this; + } + + public PromptWithArgumentParser AddInstructionsOrTemperature(object instructionsOrTemperature) + { + _instructionsOrTemperature = instructionsOrTemperature; + + return this; + } + + public PromptWithArgumentParser AddTemperature(object temperature) + { + _temperature = temperature; + + return this; + } + + public Arguments Parse() + { + var provider = _provider ?? _configuration + .GetSection(nameof(CellmConfiguration)) + .GetValue(nameof(CellmConfiguration.DefaultProvider)) + ?? throw new ArgumentException(nameof(CellmConfiguration.DefaultProvider)); + + var model = _model ?? _configuration + .GetSection(nameof(CellmConfiguration)) + .GetValue(nameof(CellmConfiguration.DefaultModel)) + ?? throw new ArgumentException(nameof(CellmConfiguration.DefaultModel)); + + var defaultTemperature = _configuration + .GetSection(nameof(CellmConfiguration)) + .GetValue(nameof(CellmConfiguration.DefaultTemperature)) + ?? throw new ArgumentException(nameof(CellmConfiguration.DefaultTemperature)); + + return (_instructionsOrContext, _instructionsOrTemperature, _temperature) switch + { + // "=PROMPT("Extract keywords") + (string instructions, ExcelMissing, ExcelMissing) => new Arguments(provider, model, string.Empty, RenderInstructions(instructions), ParseTemperature(defaultTemperature)), + // "=PROMPT("Extract keywords", 0.7) + (string instructions, double temperature, ExcelMissing) => new Arguments(provider, model, string.Empty, RenderInstructions(instructions), ParseTemperature(temperature)), + // "=PROMPT(A1:B2) + (ExcelReference context, ExcelMissing, ExcelMissing) => new Arguments(provider, model, RenderContext(ParseCells(context)), RenderInstructions(SystemMessages.InlineInstructions), ParseTemperature(defaultTemperature)), + // "=PROMPT(A1:B2, 0.7) + (ExcelReference context, double temperature, ExcelMissing) => new Arguments(provider, model, RenderContext(ParseCells(context)), RenderInstructions(SystemMessages.InlineInstructions), ParseTemperature(defaultTemperature)), + // "=PROMPT(A1:B2, "Extract keywords") + (ExcelReference context, string instructions, ExcelMissing) => new Arguments(provider, model, RenderContext(ParseCells(context)), RenderInstructions(instructions), ParseTemperature(defaultTemperature)), + // "=PROMPT(A1:B2, "Extract keywords", 0.7) + (ExcelReference context, string instructions, double temperature) => new Arguments(provider, model, RenderContext(ParseCells(context)), RenderInstructions(instructions), ParseTemperature(temperature)), + // "=PROMPT(A1:B2, C1:D2) + (ExcelReference context, ExcelReference instructions, ExcelMissing) => new Arguments(provider, model, RenderContext(ParseCells(context)), RenderInstructions(ParseCells(instructions)), ParseTemperature(defaultTemperature)), + // "=PROMPT(A1:B2, C1:D2, 0.7) + (ExcelReference context, ExcelReference instructions, double temperature) => new Arguments(provider, model, RenderContext(ParseCells(context)), RenderInstructions(ParseCells(instructions)), ParseTemperature(temperature)), + // Anything else + _ => throw new ArgumentException($"Invalid arguments ({_instructionsOrContext?.GetType().Name}, {_instructionsOrTemperature?.GetType().Name}, {_temperature?.GetType().Name})") + }; + } + + private static string GetProvider(string providerAndModel) + { + var index = providerAndModel.IndexOf("/"); + + if (index < 0) + { + throw new ArgumentException($"Provider and model argument must on the form \"Provider/Model\""); + } + + return providerAndModel[..index]; + } + + private static string GetModel(string providerAndModel) + { + var index = providerAndModel.IndexOf("/"); + + if (index < 0) + { + throw new ArgumentException($"Provider and model argument must on the form \"Provider/Model\""); + } + + return providerAndModel[(index + 1)..]; + } + + private static string GetCellAsString(ExcelReference providerAndModel) + { + if (providerAndModel.RowFirst != providerAndModel.RowLast || + providerAndModel.ColumnFirst != providerAndModel.ColumnLast) + { + throw new ArgumentException("Provider and model argument must be a string or a single cell"); + } + return providerAndModel.GetValue()?.ToString() ?? throw new ArgumentException("Provider and model argument must be a valid cell reference"); + } + + private static string ParseCells(ExcelReference reference) + { + try + { + var app = (Application)ExcelDnaUtil.Application; + var sheetName = (string)XlCall.Excel(XlCall.xlSheetNm, reference); + sheetName = sheetName[(sheetName.LastIndexOf("]") + 1)..]; + var worksheet = app.Sheets[sheetName]; + + var tableBuilder = new StringBuilder(); + var valueBuilder = new StringBuilder(); + + var rows = reference.RowLast - reference.RowFirst + 1; + var columns = reference.ColumnLast - reference.ColumnFirst + 1; + + for (int row = 0; row < rows; row++) + { + for (int column = 0; column < columns; column++) + { + var value = worksheet.Cells[reference.RowFirst + row + 1, reference.ColumnFirst + column + 1].Text; + valueBuilder.Append(value); + + tableBuilder.Append("| "); + tableBuilder.Append(GetColumnName(reference.ColumnFirst + column) + GetRowName(reference.RowFirst + row)); + tableBuilder.Append(' '); + tableBuilder.Append(value); + tableBuilder.Append(' '); + } + + tableBuilder.AppendLine("|"); + } + + if (string.IsNullOrEmpty(valueBuilder.ToString())) + { + throw new ArgumentException("Empty cells"); + } + + return tableBuilder.ToString(); + } + catch (Exception ex) + { + throw new CellmException($"Failed to parse context: {ex.Message}", ex); + } + } + + private static string GetColumnName(int columnNumber) + { + string columnName = ""; + while (columnNumber >= 0) + { + columnName = (char)('A' + columnNumber % 26) + columnName; + columnNumber = columnNumber / 26 - 1; + } + return columnName; + } + + private static string GetRowName(int rowNumber) + { + return (rowNumber + 1).ToString(); + } + + private static string RenderContext(string context) + { + return new StringBuilder() + .AppendLine("") + .AppendLine(context) + .AppendLine("") + .ToString(); + } + + private static string RenderInstructions(string instructions) + { + return new StringBuilder() + .AppendLine("") + .AppendLine(instructions) + .AppendLine("") + .ToString(); + } + + private double ParseTemperature(double temperature) + { + if (temperature < 0 || temperature > 1) + { + throw new ArgumentOutOfRangeException(nameof(temperature), "Temperature argument must be between 0 and 1"); + } + + return temperature; + } +} \ No newline at end of file diff --git a/src/Cellm/AddIn/SystemMessages.cs b/src/Cellm/AddIn/SystemMessages.cs index b4b2aef..971fbb3 100644 --- a/src/Cellm/AddIn/SystemMessages.cs +++ b/src/Cellm/AddIn/SystemMessages.cs @@ -3,24 +3,15 @@ internal static class SystemMessages { public const string SystemMessage = @" - -The user has called you via an Excel formula. -The Excel sheet is rendered as a table where each cell consist of its coordinate and value. -The table in the tag is your context and you must use it when following the user's instructions. - - - Return ONLY the result of following the user's instructions as plain text without any formatting. Your response MUST be EITHER: - A single word or number OR -- A multiple words or numbers separated by commas (,) OR +- A list of multiple words or numbers separated by commas (,) OR - A sentence Do not provide explanations, steps, or engage in conversation. - "; public const string InlineInstructions = "Analyze the context carefully and follow any instructions within the table."; - } diff --git a/src/Cellm/Services/ServiceLocator.cs b/src/Cellm/Services/ServiceLocator.cs index d60de39..d7a4d64 100644 --- a/src/Cellm/Services/ServiceLocator.cs +++ b/src/Cellm/Services/ServiceLocator.cs @@ -90,7 +90,7 @@ private static IServiceCollection ConfigureServices(IServiceCollection services) services .AddSingleton(configuration) .AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) - .AddTransient() + .AddTransient() .AddSingleton() .AddSingleton();